完成了 BOM 管理和生产管理,完成部分发料单、采购计划和调拨单。

This commit is contained in:
c
2026-02-28 18:18:01 +08:00
commit 219eef4729
399 changed files with 46113 additions and 0 deletions

View File

@@ -0,0 +1,535 @@
<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 { usePage } from "@/composables/use-page";
import BaseForm from "@/components/base/base-form/BaseForm.vue";
import { $t } from "@/common/languages";
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import { formatDate } from "@/common/utils/format-utils";
import DefaultStatusSwitchColumn from "@/components/base/default-column/DefaultStatusSwitchColumn.vue";
import BaseSelect from "@/components/base/base-select/BaseSelect.vue";
import { generateDucumentNo } from "@/common/utils/document-no-generator/document-no-generator";
import { get, post } from "@/common/http/request";
import BaseTableForm from "@/components/base/base-table-form/BaseTableForm.vue";
import { DocumentType } from "@/common/enums/DocumentType";
import { useStatus } from "@/common/languages/mapping/base-info-mapping";
/**
* 必须要的变量
*/
const getPageUrl = "/production/productionPlan/getProductionPlanPage";
const addUrl = "/production/productionPlan/addProductionPlan";
const editUrl = "/production/productionPlan/updateProductionPlan";
const removeUrl = "/production/productionPlan/deleteProductionPlan";
const getProjectSelectList = "/production/bom/getBomSelectList";
const getWarehouseSelectListUrl = "/warehouse/warehouse/getWarehouseSelectList";
const getProductionPlanShortageListUrl = "/production/productionPlan/getMaterialShortageList";
const generatePurchasePlanUrl = "/production/productionPlan/generatePurchasePlan";
const generateTransferOrderUrl = "/production/productionPlan/generateTransferOrder";
const generateProductionIssueUrl = "/production/productionPlan/generateProductionIssue";
const searchers = [
{ name: "projectName", type: "text" as const, placeholder: $t("_prop.production.production_plan.projectName") },
];
const rules = reactive<FormRules>({
projectId: [{ required: true, message: $t("_message.production.production_plan.select_projectId"), trigger: "blur" }],
productionNum: [
{ required: true, message: $t("_message.production.production_plan.input_productionNum"), trigger: "blur" },
],
storeNo: [{ required: true, message: $t("_message.production.production_plan.select_storeNo"), trigger: "blur" }],
productionCount: [
{ required: true, message: $t("_message.production.production_plan.input_productionCount"), trigger: "blur" },
],
productionMark: [
{ required: false, message: $t("_message.production.production_plan.input_productionMark"), trigger: "blur" },
],
productionNote: [
{ required: false, message: $t("_message.production.production_plan.input_productionNote"), trigger: "blur" },
],
});
/**
* 基本不变通用变量
*/
const tableRef = ref<InstanceType<typeof BasePageableTable> | null>(null);
const { useAdd, useEdit, useRemove, useGeneralPageRef } = usePage(tableRef);
const { title, visible, formType, form } = useGeneralPageRef();
const { getProductionPlanStatusLabel } = useStatus();
const bomSelectRef = ref<InstanceType<typeof BaseSelect>>();
const warehouseSelectRef = ref<InstanceType<typeof BaseSelect>>();
const transferWarehouseSelectRef = ref<InstanceType<typeof BaseSelect>>();
const dialogRef = ref<InstanceType<typeof BaseTableForm>>();
const dialogVisible = ref(false);
const shortageData = ref([]);
const dialogTableData = ref([]);
const dialogSelectable = ref(false);
const transferWarehouse = ref();
const issueDialogVisible = ref(false);
/**
* 可以自定义的变量
*/
const add = () => {
form.value = {};
title.value = "_title.production.production_plan.add";
form.value["productionNum"] = generateDucumentNo("SCJH");
visible.value = true;
formType.value = false;
};
const edit = (row: any) => {
title.value = "_title.production.production_plan.edit";
form.value = { ...row };
visible.value = true;
formType.value = true;
};
const remove = (row: any) => {
useRemove(removeUrl, row.id, "_message.production.production_plan.delete_message");
};
const submit = (form: any, formRef: FormInstance | undefined) => {
setName();
if (formRef !== undefined) {
formRef.validate(valid => {
if (valid) {
if (formType.value) useEdit(editUrl, form, visible);
else useAdd(addUrl, form, visible);
}
});
}
};
const validateSelection = (requireStatusZero = false) => {
const rows = tableRef.value?.tableMainRef?.tableRef?.getSelectionRows();
if (!rows || rows.length === 0) {
ElMessage.warning($t("_message.production.production_plan.select_production_plan"));
return null;
}
if (requireStatusZero && !rows.every(row => row.productionStatus === 0)) {
ElMessage.warning($t("_message.production.production_plan.error_production_status"));
return null;
}
const warehouseId = rows[0].storeNo;
if (!rows.every(item => item.storeNo === warehouseId)) {
ElMessage.warning($t("_message.production.production_plan.duplicate_store_no"));
return null;
}
return rows;
};
const buildDocumentInfo = (rows: any[], type: DocumentType | string, markSuffixKey: string, nameSuffixKey: string) => {
const code = generateDucumentNo(type);
const productionNums = rows.map((item: any) => item.productionNum).join(",");
const projectNames = rows.map((item: any) => item.projectName).join(",");
return {
code,
mark:
$t("_message.common.generate_document_mark_prefix") +
productionNums +
$t(markSuffixKey) +
code +
$t("_message.common.generate_document_mark_suffix"),
name: projectNames + $t(nameSuffixKey),
};
};
const viewMaterialShortage = async () => {
const rows = validateSelection(true); // 需要校验状态为 0
if (!rows) return;
const params = new URLSearchParams();
rows.forEach(({ id }) => params.append("ids", id));
shortageData.value = await get(getProductionPlanShortageListUrl, params).then(res => res.data);
calculateDiffQty();
dialogRef.value?.clearSelection();
dialogTableData.value = shortageData.value;
dialogSelectable.value = false;
dialogVisible.value = true;
};
const generatePurchasePlan = (selectedShortageRows: any[]) => {
ElMessageBox.confirm($t("_message.production.production_plan.generate_purchase_plan_confirm"), $t("_level.warning"), {
confirmButtonText: $t("_button.confirm"),
cancelButtonText: $t("_button.cancel"),
type: "warning",
}).then(async () => {
const rows = validateSelection();
if (!rows) return;
const {
code: planNo,
mark: planMark,
name: planName,
} = buildDocumentInfo(
rows,
"PA",
"_message.production.production_plan.generate_purchase_plan_mark_suffix",
"_message.production.production_plan.generate_purchase_plan_name_suffix"
);
const plan = {
planNo,
storeNo: rows[0].storeNo,
storeName: rows[0].storeName,
remask: planMark,
planName,
planItems: selectedShortageRows.map(({ partNumber, diffQty, id }) => ({
partNumber,
purchaseCount: -diffQty,
partId: id,
})),
};
try {
const res = await post(generatePurchasePlanUrl, plan);
ElMessage[res.code === 0 ? "success" : "error"](
res.code === 0
? $t("_message.production.production_plan.generate_purchase_plan_success")
: $t("_message.production.production_plan.generate_purchase_plan_fail") + res.msg
);
} catch (err) {
ElMessage.error($t("_message.production.production_plan.generate_purchase_plan_fail") + err);
}
});
};
const generateTransferOrder = (selectedShortageRows: any[]) => {
ElMessageBox.confirm(
$t("_message.production.production_plan.generate_transfer_order_confirm"),
$t("_level.warning"),
{
confirmButtonText: $t("_button.confirm"),
cancelButtonText: $t("_button.cancel"),
type: "warning",
}
).then(async () => {
const rows = validateSelection();
if (!rows) return;
const outWarehouseId = transferWarehouse.value;
if (!outWarehouseId) {
ElMessage.warning("仓库不能为空");
return;
}
const outWarehouseName = transferWarehouseSelectRef.value?.getLabel();
if (rows[0].storeNo === outWarehouseId) {
ElMessage.warning("仓库不能相同");
return;
}
const {
code: formCode,
mark: formMark,
name: formName,
} = buildDocumentInfo(
rows,
DocumentType.StockTransferOrder,
"_message.production.production_plan.generate_transfer_order_mark_suffix",
"_message.production.production_plan.generate_transfer_order_name_suffix"
);
const plan = {
formCode,
formName,
formMark,
storeNo: rows[0].storeNo,
storeName: rows[0].storeName,
outStoreNo: outWarehouseId,
outStoreName: outWarehouseName,
transferOrderItems: selectedShortageRows.map(({ partNumber, diffQty, productSpec, id }) => ({
partNumber,
productCount: -diffQty,
productSpec,
partId: id,
})),
};
try {
const res = await post(generateTransferOrderUrl, plan);
ElMessage[res.code === 0 ? "success" : "error"](
res.code === 0
? $t("_message.production.production_plan.generate_transfer_order_success")
: $t("_message.production.production_plan.generate_transfer_order_fail") + res.msg
);
} catch (err) {
ElMessage.error($t("_message.production.production_plan.generate_transfer_order_fail") + err);
}
});
};
const generateProductionIssue = async () => {
const rows = validateSelection(true); // 需要校验状态为 0
if (!rows) return;
const params = new URLSearchParams();
rows.forEach(({ id }) => params.append("ids", id));
dialogTableData.value = await get(getProductionPlanShortageListUrl, params).then(res => res.data);
setActualQtyAndDiffQty();
issueDialogVisible.value = true;
};
const _generateProductionIssue = async () => {
ElMessageBox.confirm(
$t("_message.production.production_plan.generate_production_issue_confirm"),
$t("_level.warning"),
{
confirmButtonText: $t("_button.confirm"),
cancelButtonText: $t("_button.cancel"),
type: "warning",
}
).then(async () => {
const rows = validateSelection();
if (!rows) return;
const {
code: formCode,
mark: formMark,
name: formName,
} = buildDocumentInfo(
rows,
DocumentType.StockTransferOrder,
"_message.production.production_plan.generate_production_issue_mark_suffix",
"_message.production.production_plan.generate_production_issue_name_suffix"
);
const issue = {
formCode,
formName,
formMark,
storeNo: rows[0].storeNo,
storeName: rows[0].storeName,
items: dialogTableData.value.map(({ partNumber, storeNo, actualQty, requiredQty }) => ({
partNumber,
storeNo,
productCount: actualQty,
demandCount: requiredQty,
})),
};
try {
const res = await post(generateProductionIssueUrl, { ids: rows.map(r => r.id), issue });
if (res.code === 0) {
issueDialogVisible.value = false;
tableRef.value?.reload();
ElMessage.success($t("_message.production.production_plan.generate_production_issue_success"));
} else {
ElMessage.error($t("_message.production.production_plan.generate_production_issue_fail") + res.msg);
}
} catch (err) {
ElMessage.error($t("_message.production.production_plan.generate_production_issue_fail") + err);
}
});
};
const calculateDiffQty = () => {
shortageData.value.forEach((item: any) => {
item.diffQty = computed(() => item.stockQty - item.requiredQty);
});
};
const setActualQtyAndDiffQty = () => {
dialogTableData.value.forEach((item: any) => {
item.actualQty = item.requiredQty;
item.diffQty = computed(() => item.stockQty - item.requiredQty);
});
};
const handleShortageFilterSubmit = () => {
dialogTableData.value = shortageData.value.filter(item => (item as any).diffQty < 0);
dialogSelectable.value = true;
};
const handleUnfilterSubmit = () => {
dialogTableData.value = shortageData.value;
dialogSelectable.value = false;
};
const topButtonClick = (eventName: string) => {
switch (eventName) {
case "add":
add();
break;
case "viewMaterialShortage":
viewMaterialShortage();
break;
case "generateProductionIssue":
generateProductionIssue();
break;
}
};
const operateButtonClick = (eventName: string, row: any) => {
switch (eventName) {
case "edit":
edit(row);
break;
case "remove":
remove(row);
break;
}
};
const dialogButtonClick = (eventName: string, btn: any, rows: any[]) => {
switch (eventName) {
case "generatePurchasePlan":
generatePurchasePlan(rows);
break;
case "generateTransferOrder":
generateTransferOrder(rows);
break;
}
};
const setName = () => {
form.value["projectName"] = bomSelectRef.value?.getLabel();
form.value["storeName"] = warehouseSelectRef.value?.getLabel();
};
const handleReset = () => {
form.value["productionNum"] = generateDucumentNo("SCJH");
};
</script>
<template>
<BasePageableTable :url="getPageUrl" :searchers="searchers" ref="tableRef">
<template #tool-button>
<DefaultToolButton @top-button-click="topButtonClick" />
</template>
<template #columns>
<el-table-column prop="id" type="selection" width="40" />
<el-table-column :label="$t('_prop.production.production_plan.productionNum')" prop="productionNum" />
<el-table-column :label="$t('_prop.production.production_plan.projectName')" prop="projectName" />
<el-table-column :label="$t('_prop.production.production_plan.storeName')" prop="storeName" />
<el-table-column :label="$t('_prop.production.production_plan.productionCount')" prop="productionCount" />
<el-table-column :label="$t('_prop.production.production_plan.productionMark')" prop="productionMark" />
<el-table-column :label="$t('_prop.common.createDate')" prop="createDate" :formatter="formatDate" />
<DefaultStatusSwitchColumn
status-param-name="productionStatus"
:status-label-mapping="getProductionPlanStatusLabel"
/>
<DefaultOperateButtonColumn @operate-button-click="operateButtonClick" />
</template>
</BasePageableTable>
<BaseForm
v-model:visible="visible"
@submit="submit"
v-model:form="form"
:title="$t(title)"
:rules="rules"
@reset="handleReset"
>
<template #form-items>
<el-form-item prop="id" v-if="false"><el-input v-model="form.id" /></el-form-item>
<el-form-item prop="projectName" v-if="false"><el-input v-model="form.projectName" /></el-form-item>
<el-form-item prop="storeName" v-if="false"><el-input v-model="form.storeName" /></el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('_prop.production.production_plan.projectId')" prop="projectId">
<BaseSelect v-model="form.projectId" :url="getProjectSelectList" ref="bomSelectRef" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('_prop.production.production_plan.productionNum')" prop="productionNum">
<el-input
v-model="form.productionNum"
:placeholder="$t('_message.production.production_plan.input_productionNum')"
disabled
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('_prop.production.production_plan.storeNo')" prop="storeNo">
<BaseSelect v-model="form.storeNo" :url="getWarehouseSelectListUrl" ref="warehouseSelectRef" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('_prop.production.production_plan.productionCount')" prop="productionCount">
<el-input-number
:min="1"
v-model="form.productionCount"
:placeholder="$t('_message.production.production_plan.input_productionCount')"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('_prop.production.production_plan.productionMark')" prop="productionMark">
<el-input
v-model="form.productionMark"
type="textarea"
autosize
:placeholder="$t('_message.production.production_plan.input_productionMark')"
/>
</el-form-item>
<el-form-item :label="$t('_prop.production.production_plan.productionNote')" prop="productionNote">
<el-input
v-model="form.productionNote"
type="textarea"
autosize
:placeholder="$t('_message.production.production_plan.input_productionNote')"
/>
</el-form-item>
</template>
</BaseForm>
<BaseTableForm
ref="dialogRef"
v-model:visible="dialogVisible"
:title="$t('_title.production.production_plan.dialog')"
:tableData="dialogTableData"
:selectable="dialogSelectable"
row-key="partNumber"
@dialog-button-click="dialogButtonClick"
>
<template #filter>
<el-button type="danger" @click="handleShortageFilterSubmit">{{ $t("_button.shortageFilterButton") }}</el-button>
<el-button type="primary" @click="handleUnfilterSubmit">{{ $t("_button.unfilterButton") }}</el-button>
</template>
<template #table-columns>
<el-table-column :label="$t('_prop.production.bom_item.partNumber')" prop="partNumber" />
<el-table-column :label="$t('_prop.production.bom_item.productSpecs')" prop="productSpecs" />
<el-table-column :label="$t('_prop.production.production_plan.requiredQty')" prop="requiredQty">
<template #default="{ row }">
<el-input-number v-model="row.requiredQty" :min="0" />
</template>
</el-table-column>
<el-table-column :label="$t('_prop.production.production_plan.stockQty')" prop="stockQty" />
<el-table-column :label="$t('_prop.production.production_plan.diffQty')" prop="diffQty" />
</template>
<template #attachment="{ checkPermission }">
<div v-if="checkPermission('generateTransferOrder')" style="display: flex; align-items: center">
<span style="width: 200px; padding-right: 20px">{{ $t("_prop.production.production_plan.outStoreNo") }}</span>
<BaseSelect v-model="transferWarehouse" :url="getWarehouseSelectListUrl" ref="transferWarehouseSelectRef" />
</div>
</template>
</BaseTableForm>
<BaseTableForm
ref="dialogRef"
v-model:visible="issueDialogVisible"
:title="$t('_title.production.production_plan.issueDialog')"
:tableData="dialogTableData"
:selectable="false"
:useAuth="false"
>
<template #table-columns>
<el-table-column :label="$t('_prop.production.bom_item.partNumber')" prop="partNumber" />
<el-table-column :label="$t('_prop.production.bom_item.productSpecs')" prop="productSpecs" />
<el-table-column :label="$t('_prop.production.production_plan.requiredQty')" prop="requiredQty" />
<el-table-column :label="$t('_prop.production.production_plan.actualQty')" prop="actualQty">
<template #default="{ row }">
<el-input-number v-model="row.actualQty" :min="0" />
</template>
</el-table-column>
<el-table-column :label="$t('_prop.production.production_plan.stockQty')" prop="stockQty" />
<el-table-column :label="$t('_prop.production.production_plan.diffQty')" prop="diffQty" />
</template>
<template #attachment>
<el-button type="primary" @click="_generateProductionIssue">
{{ $t("_button.submit") }}
</el-button>
</template>
</BaseTableForm>
</template>