Files
erp-frontend/src/views/purchase/purchase-plan/PurchasePlanView.vue

582 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import BasePageableTable from "@/components/base/base-pageable-table/BasePageableTable.vue";
import DefaultToolButton from "@/components/base/default-tool-button/DefaultToolButton.vue";
import DefaultOperateButtonColumn from "@/components/base/default-column/DefaultOperateButtonColumn.vue";
import DefaultStatusSwitchColumn from "@/components/base/default-column/DefaultStatusSwitchColumn.vue";
import { usePage } from "@/composables/use-page";
import BaseFormWithTable from "@/components/base/base-form-with-table/BaseFormWithTable.vue";
import BaseSelect from "@/components/base/base-select/BaseSelect.vue";
import BaseTableForm from "@/components/base/base-table-form/BaseTableForm.vue";
import { $t } from "@/common/languages";
import type { FormInstance, FormRules } from "element-plus";
import { formatDate } from "@/common/utils/format-utils";
import { get, post } from "@/common/http/request";
import { ElMessage } from "element-plus";
import type { FieldMappingConfig } from "@/components/base/base-form-with-table/type";
import { ref } from "vue";
import { useStatus } from "@/common/languages/mapping/base-info-mapping";
import Decimal from "decimal.js";
/**
* 必须要的变量
*/
const getPageUrl = "/purchase/purchaseplan/getPurchasePlanPage";
const addUrl = "/purchase/purchaseplan/addPurchasePlan";
const editUrl = "/purchase/purchaseplan/updatePurchasePlan";
const removeUrl = "/purchase/purchaseplan/deletePurchasePlan";
const getItemsUrl = "/purchase/purchaseplan/getPurchasePlanItemsWithVendorSuggestions";
const generateOrderUrl = "/purchase/purchaseplan/generatePurchaseOrder";
const searchers = [
{ name: "vendorName", type: "text" as const, placeholder: $t("_prop.purchase.purchase_plan.vendorName") },
];
const { getPurchasePlanStatusLabel, getPurchasePlanItemStatusLabel } = useStatus();
const rules = reactive<FormRules>({
planName: [{ required: true, message: $t("_message.purchase.purchase_plan.input_planName"), trigger: "blur" }],
vendorId: [{ required: true, message: $t("_message.purchase.purchase_plan.select_vendor"), trigger: "blur" }],
storeNo: [{ required: true, message: $t("_message.purchase.purchase_plan.select_store"), trigger: "blur" }],
planItems: [
{
required: true,
validator: (rule, value, callback) => {
if (value === undefined || value.length === 0) {
callback($t("_message.purchase.purchase_plan.upload_planItems"));
}
callback();
},
trigger: "change",
},
],
});
const itemArrayName = "planItems";
/**
* 基本不变通用变量
*/
const tableRef = ref<InstanceType<typeof BasePageableTable> | null>(null);
const { useAdd, useEdit, useRemove, useGeneralPageRef } = usePage(tableRef);
const { title, visible, formType, form } = useGeneralPageRef();
const baseFormWithTableRef = ref<InstanceType<typeof BaseFormWithTable>>();
const generateOrderVisible = ref(false);
const generateOrderForm = reactive({
planId: 0,
planStatus: 0,
});
const planItems = ref([] as any[]);
const allVendors = ref([] as any[]);
const partVendorMappings = ref([] as any[]);
const NOT_PURCHASE_VENDOR_ID = -1;
const add = () => {
form.value = {
planItems: [],
};
title.value = "_title.purchase.purchase_plan.add";
visible.value = true;
formType.value = false;
};
const edit = (row: any) => {
title.value = "_title.purchase.purchase_plan.edit";
form.value = { ...row };
visible.value = true;
formType.value = true;
};
const remove = (row: any) => {
useRemove(removeUrl, row.id, "_message.purchase.purchase_plan.delete_message");
};
const submit = (form: any, formRef: FormInstance | undefined) => {
if (formRef !== undefined) {
formRef.validate(valid => {
if (valid) {
if (formType.value) useEdit(editUrl, form, visible);
else useAdd(addUrl, form, visible);
}
});
}
};
const topButtonClick = (eventName: string) => {
switch (eventName) {
case "add":
add();
break;
}
};
const authShowFunc = (row: any, button: globalThis.ButtonProp) => {
if (row.planStatus === 2 && button.eventName === "generatePurchaseOrder") {
return false;
}
return true;
};
const operateButtonClick = (eventName: string, row: any) => {
switch (eventName) {
case "edit":
edit(row);
break;
case "remove":
remove(row);
break;
case "generatePurchaseOrder":
showGenerateOrderDialog(row);
break;
}
};
const buildVendorSuggestions = (item: any) => {
const suggestions: any[] = [];
const addedVendorIds = new Set<number>();
const vendorCode1 = item.vendorCode1;
const vendorCode2 = item.vendorCode2;
const vendorCode3 = item.vendorCode3;
const vendorCodes = [vendorCode1, vendorCode2, vendorCode3];
for (let i = 0; i < vendorCodes.length; i++) {
if (vendorCodes[i] != null && !addedVendorIds.has(vendorCodes[i])) {
const vendor = allVendors.value.find((v: any) => v.vendorId === vendorCodes[i]);
if (vendor) {
const mapping = partVendorMappings.value.find(
(m: any) => m.partNumber === item.partNumber && m.vendorId === vendor.vendorId
);
suggestions.push({
vendorId: vendor.vendorId,
vendorName: vendor.vendorName,
priority: i + 1,
price: mapping?.costPrice || 0,
});
addedVendorIds.add(vendor.vendorId);
}
}
}
const itemMappings = partVendorMappings.value.filter((m: any) => m.partNumber === item.partNumber);
for (const mapping of itemMappings) {
if (!addedVendorIds.has(mapping.vendorId)) {
const vendor = allVendors.value.find((v: any) => v.vendorId === mapping.vendorId);
if (vendor) {
suggestions.push({
vendorId: vendor.vendorId,
vendorName: vendor.vendorName,
priority: 2,
price: mapping.costPrice || 0,
});
addedVendorIds.add(vendor.vendorId);
}
}
}
for (const vendor of allVendors.value) {
if (!addedVendorIds.has(vendor.vendorId)) {
suggestions.push({
vendorId: vendor.vendorId,
vendorName: vendor.vendorName,
priority: 3,
price: 0,
});
addedVendorIds.add(vendor.vendorId);
}
}
suggestions.push({
vendorId: NOT_PURCHASE_VENDOR_ID,
vendorName: "暂不采购",
priority: 99,
price: 0,
});
return suggestions;
};
const showGenerateOrderDialog = async (row: any) => {
generateOrderForm.planId = row.id;
generateOrderForm.planStatus = row.planStatus;
try {
const response = await get(getItemsUrl, { planId: row.id });
const data = response.data;
allVendors.value = data.allVendors || [];
partVendorMappings.value = data.partVendorMappings || [];
planItems.value = (data.items || []).map((item: any) => {
const currentCount = item.currentCount || item.purchaseCount || 1;
const price = item.price || 0;
return {
...item,
currentCount,
price,
total: new Decimal(currentCount).mul(price).toNumber(),
vendorSuggestions: [],
calculateTotal: function () {
this.total = new Decimal(this.currentCount).mul(this.price).toNumber();
},
};
});
for (const item of planItems.value) {
item.vendorSuggestions = buildVendorSuggestions(item);
if (item.completeStatus === 0 && item.vendorSuggestions.length > 0) {
const firstSuggestion = item.vendorSuggestions[0];
if (firstSuggestion.vendorId !== NOT_PURCHASE_VENDOR_ID) {
item.vendorId = firstSuggestion.vendorId;
item.price = firstSuggestion.price || 0;
item.calculateTotal();
}
} else if (item.completeStatus !== 0 && item.vendorId) {
const vendor = allVendors.value.find((v: any) => v.vendorId === item.vendorId);
if (vendor) {
item.vendorName = vendor.vendorName;
}
}
}
planItems.value.sort((a, b) => a.completeStatus - b.completeStatus);
generateOrderVisible.value = true;
} catch (error) {
ElMessage.error($t("_message.purchase.purchase_plan.get_items_error") + error);
}
};
const onVendorChange = (item: any, vendorId: number) => {
if (item.vendorSuggestions && item.vendorSuggestions.length > 0) {
const selectedVendor = item.vendorSuggestions.find((v: any) => v.vendorId === vendorId);
if (selectedVendor) {
if (selectedVendor.vendorId !== NOT_PURCHASE_VENDOR_ID) {
item.price = selectedVendor.price || 0;
}
item.calculateTotal();
}
}
};
const generatePurchaseOrder = async () => {
const validItems = planItems.value.filter(
item => item.vendorId && item.vendorId !== NOT_PURCHASE_VENDOR_ID && item.completeStatus === 0
);
if (validItems.length === 0) {
ElMessage.warning($t("_message.purchase.purchase_plan.select_vendor"));
return;
}
try {
const vendorGroups = new Map<number, any[]>();
validItems.forEach(item => {
if (!vendorGroups.has(item.vendorId)) {
vendorGroups.set(item.vendorId, []);
}
vendorGroups.get(item.vendorId)!.push(item);
});
const now = new Date();
const baseFormCode =
"PO" +
now.getFullYear() +
String(now.getMonth() + 1).padStart(2, "0") +
String(now.getDate()).padStart(2, "0") +
String(now.getHours()).padStart(2, "0") +
String(now.getMinutes()).padStart(2, "0") +
String(now.getSeconds()).padStart(2, "0");
const promises: Promise<any>[] = [];
let orderIndex = 1;
vendorGroups.forEach((items, vendorId) => {
const vendor = allVendors.value.find((v: any) => v.vendorId === vendorId);
const vendorName = vendor ? vendor.vendorName : "";
const formCode = baseFormCode + String(orderIndex).padStart(2, "0");
const formName = `采购订单-${vendorName}-${formCode}`;
const formMark = `采购计划【${generateOrderForm.planNo}】生成`;
const totalValue = items.reduce((sum, item) => {
return new Decimal(sum).add(new Decimal(item.currentCount).mul(item.price)).toNumber();
}, 0);
const data = {
planId: generateOrderForm.planId,
vendorId: vendorId,
vendorName: vendorName,
formCode: formCode,
formName: formName,
formMark: formMark,
totalValue: totalValue,
selectedItems: items,
};
promises.push(post(generateOrderUrl, data));
orderIndex++;
});
await Promise.all(promises);
ElMessage.success($t("_message.purchase.purchase_plan.generate_order_success"));
generateOrderVisible.value = false;
tableRef.value?.reload();
} catch (err: any) {
ElMessage.error(err.response?.data?.message || $t("_message.purchase.purchase_plan.generate_order_error"));
}
};
const mappingConfig: FieldMappingConfig = {
partNumber: {
sourceKey: "商品编号",
defaultValue: "",
},
purchaseCount: {
sourceKey: "计划数量",
defaultValue: 0,
transform: (val: any) => {
const num = Number(val);
return isNaN(num) ? 0 : num;
},
},
price: {
sourceKey: "单价",
defaultValue: 0,
transform: (val: any) => {
const num = Number(val);
return isNaN(num) ? 0 : num;
},
},
currentCount: {
sourceKey: "本次采购数量",
defaultValue: 0,
transform: (val: any) => {
const num = Number(val);
return isNaN(num) ? 0 : num;
},
},
};
</script>
<template>
<BasePageableTable :url="getPageUrl" :searchers="searchers" ref="tableRef">
<template #tool-button>
<DefaultToolButton @top-button-click="topButtonClick" />
</template>
<template #columns>
<el-table-column :label="$t('_prop.purchase.purchase_plan.planNo')" prop="planNo" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.planName')" prop="planName" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.storeName')" prop="storeName" />
<DefaultStatusSwitchColumn status-param-name="planStatus" :status-label-mapping="getPurchasePlanStatusLabel" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.remask')" prop="remask" />
<el-table-column :label="$t('_prop.common.createDate')" prop="createDate" :formatter="formatDate" />
<DefaultOperateButtonColumn @operate-button-click="operateButtonClick" :auth-show-func="authShowFunc" />
</template>
</BasePageableTable>
<BaseFormWithTable
ref="baseFormWithTableRef"
v-model:visible="visible"
@submit="submit"
v-model:form="form"
:title="$t(title)"
:rules="rules"
:base-title="$t('_title.purchase.purchase_plan.baseTitle')"
:table-title="$t('_title.purchase.purchase_plan.tableTitle')"
item-array-name="planItems"
upload-desc="采购明细"
:mapping-config="mappingConfig"
>
<template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item :label="$t('_prop.purchase.purchase_plan.planName')" prop="planName">
<el-input v-model="form.planName" :placeholder="$t('_message.purchase.purchase_plan.input_planName')" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('_prop.purchase.purchase_plan.vendorName')" prop="vendorId">
<BaseSelect
v-model="form.vendorId"
:url="'/sys/vendor/getVendorList'"
name="id"
label="vendorName"
:placeholder="$t('_message.purchase.purchase_plan.select_vendor')"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('_prop.purchase.purchase_plan.storeName')" prop="storeNo">
<BaseSelect
v-model="form.storeNo"
:url="'/warehouse/store/getStoreList'"
name="storeNo"
label="storeName"
:placeholder="$t('_message.purchase.purchase_plan.select_store')"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('_prop.purchase.purchase_plan.remask')" prop="remask">
<el-input
v-model="form.remask"
:placeholder="$t('_message.purchase.purchase_plan.input_remask')"
type="textarea"
autosize
/>
</el-form-item>
</template>
<template #form-table-columns>
<el-table-column :label="$t('_prop.purchase.purchase_plan_item.partNumber')" width="150">
<template #default="{ row, $index }">
<el-form-item
:prop="`${itemArrayName}.${$index}.partNumber`"
:rules="[
{ required: true, message: $t('_message.purchase.purchase_plan_item.input_partNumber'), trigger: 'blur' },
]"
>
<el-input v-model="row.partNumber" size="small" />
</el-form-item>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.purchase.purchase_plan_item.purchaseCount')" width="150">
<template #default="{ row, $index }">
<el-form-item
:prop="`${itemArrayName}.${$index}.purchaseCount`"
:rules="[
{
required: true,
message: $t('_message.purchase.purchase_plan_item.input_purchaseCount'),
trigger: 'blur',
},
]"
>
<el-input-number v-model="row.purchaseCount" size="small" :min="1" />
</el-form-item>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.purchase.purchase_plan_item.price')" width="150">
<template #default="{ row, $index }">
<el-form-item
:prop="`${itemArrayName}.${$index}.price`"
:rules="[
{ required: true, message: $t('_message.purchase.purchase_plan_item.input_price'), trigger: 'blur' },
]"
>
<el-input-number v-model="row.price" size="small" :min="0" :step="0.01" />
</el-form-item>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.purchase.purchase_plan_item.currentCount')" width="150">
<template #default="{ row, $index }">
<el-form-item
:prop="`${itemArrayName}.${$index}.currentCount`"
:rules="[
{
required: true,
message: $t('_message.purchase.purchase_plan_item.input_currentCount'),
trigger: 'blur',
},
]"
>
<el-input-number v-model="row.currentCount" size="small" :min="1" />
</el-form-item>
</template>
</el-table-column>
</template>
</BaseFormWithTable>
<!-- 生成采购订单对话框 -->
<BaseTableForm
v-model:visible="generateOrderVisible"
:title="$t('_title.purchase.purchase_plan.generateOrder')"
:table-data="planItems"
:show-summary="false"
:selectable="false"
:use-auth="false"
>
<!-- 自定义表格列 -->
<template #table-columns>
<el-table-column :label="$t('_prop.purchase.purchase_plan_item.partNumber')" prop="partNumber" width="150" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.model')" prop="productSpecs" width="150" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.vendorName')" width="180">
<template #default="{ row }">
<template v-if="row.completeStatus === 0">
<el-select
v-model="row.vendorId"
:placeholder="$t('_message.purchase.purchase_plan.select_vendor')"
style="width: 100%"
@change="onVendorChange(row, row.vendorId)"
:disabled="row.completeStatus !== 0"
>
<el-option
v-for="vendor in row.vendorSuggestions"
:key="vendor.vendorId"
:label="vendor.vendorName"
:value="vendor.vendorId"
>
<span>{{ vendor.vendorName }}</span>
<span v-if="vendor.priority === 1" style="margin-left: 8px; color: #67c23a">(优先)</span>
<span v-if="vendor.priority === 99" style="margin-left: 8px; color: #909399">(跳过)</span>
<span
v-if="vendor.price && vendor.price > 0 && vendor.priority !== 99"
style="margin-left: 8px; color: #909399"
>
¥{{ vendor.price }}
</span>
</el-option>
</el-select>
</template>
<template v-else>
<span>{{ row.vendorName }}</span>
</template>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.purchase.purchase_plan.demandQuantity')" prop="purchaseCount" width="120" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.packQuantity')" prop="productPacking" width="120" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.purchaseQuantity')" width="150">
<template #default="{ row }">
<el-input-number
v-model="row.currentCount"
:min="1"
@change="row.calculateTotal()"
style="width: 100%"
:disabled="row.completeStatus !== 0"
/>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.purchase.purchase_plan.unitPrice')" width="130">
<template #default="{ row }">
<el-input-number
v-model="row.price"
:min="0"
:step="0.01"
@change="row.calculateTotal()"
style="width: 100%"
:disabled="row.completeStatus !== 0"
/>
</template>
</el-table-column>
<el-table-column :label="$t('_prop.purchase.purchase_plan.totalPrice')" prop="total" width="120" />
<el-table-column :label="$t('_prop.purchase.purchase_plan.purchaseStatus')" width="120">
<template #default="{ row }">
<span>{{ getPurchasePlanItemStatusLabel(row.completeStatus) }}</span>
</template>
</el-table-column>
</template>
<!-- 底部附加内容自定义按钮 -->
<template #attachment>
<el-button
type="primary"
:disabled="
!planItems.some(
item => item.vendorId && item.vendorId !== NOT_PURCHASE_VENDOR_ID && item.completeStatus === 0
)
"
@click="generatePurchaseOrder"
>
{{ $t("_button.confirm") }}
</el-button>
</template>
</BaseTableForm>
</template>