feat: 完成了物料总表,生产发料,入料,以及部分采购计划。

This commit is contained in:
c
2026-03-06 15:04:57 +08:00
parent 219eef4729
commit b12b758be2
27 changed files with 3681 additions and 685 deletions

View File

@@ -2,11 +2,20 @@
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 BaseForm from "@/components/base/base-form/BaseForm.vue";
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";
/**
* 必须要的变量
@@ -16,34 +25,65 @@ 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.purchaseplan.vendorName") },
{ name: "vendorName", type: "text" as const, placeholder: $t("_prop.purchase.purchase_plan.vendorName") },
];
const rules = reactive<FormRules>({});
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 = {};
title.value = "_title.purchase.purchaseplan.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.purchaseplan.edit";
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.purchaseplan.delete_message");
useRemove(removeUrl, row.id, "_message.purchase.purchase_plan.delete_message");
};
const submit = (form: any, formRef: FormInstance | undefined) => {
if (formRef !== undefined) {
@@ -62,6 +102,14 @@ const topButtonClick = (eventName: string) => {
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":
@@ -70,8 +118,240 @@ const operateButtonClick = (eventName: string, row: any) => {
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">
@@ -79,19 +359,223 @@ const operateButtonClick = (eventName: string, row: any) => {
<DefaultToolButton @top-button-click="topButtonClick" />
</template>
<template #columns>
<el-table-column prop="id" type="hidden" width="40" />
<el-table-column :label="$t('_prop.purchase.purchaseplan.planNo')" prop="planNo" />
<el-table-column :label="$t('_prop.purchase.purchaseplan.planName')" prop="planName" />
<el-table-column :label="$t('_prop.purchase.purchaseplan.storeName')" prop="storeName" />
<el-table-column :label="$t('_prop.purchase.purchaseplan.planStatus')" prop="planStatus" />
<el-table-column :label="$t('_prop.purchase.purchaseplan.remask')" prop="remask" />
<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" />
<DefaultOperateButtonColumn @operate-button-click="operateButtonClick" :auth-show-func="authShowFunc" />
</template>
</BasePageableTable>
<BaseForm v-model:visible="visible" @submit="submit" v-model:form="form" :title="$t(title)" :rules="rules">
<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>
</BaseForm>
<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>