src/api/productionManagement/productionOrder.js
@@ -73,7 +73,7 @@ // 生产订单-保存领料台账 export function saveMaterialPickingLedger(data) { return request({ url: "/productOrderMaterial/save", url: "/productOrderMaterial/update", method: "post", data, }); src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -2,47 +2,29 @@ <div> <el-dialog v-model="dialogVisible" title="领料详情" width="1400px" @close="handleClose"> <el-table v-loading="materialDetailLoading" :data="materialDetailTableData" border row-key="id"> <el-table-column label="工序名称" prop="processName" min-width="180" /> <el-table-column label="原料名称" prop="materialName" min-width="160" /> <el-table-column label="原料型号" prop="materialModel" min-width="180" /> <el-table-column label="需求数量" prop="requiredQty" min-width="110" /> <el-table-column label="原纸需要量" prop="basePaperQty" min-width="120" /> <el-table-column label="纸箱需要量" prop="cartonQty" min-width="120" /> <el-table-column label="塑料袋数量" prop="plasticBagQty" min-width="120" /> <el-table-column label="计量单位" prop="unit" width="100" /> <el-table-column label="领用数量" prop="pickQty" min-width="110" /> <el-table-column label="补料数量" min-width="120"> <template #default="{ row }"> <el-button type="primary" link @click="handleViewSupplementRecord(row)"> {{ row.supplementQty ?? 0 }} </el-button> </template> </el-table-column> <el-table-column label="原纸领用数量" prop="basePaperPickQty" min-width="120" /> <el-table-column label="纸箱领用数量" prop="cartonPickQty" min-width="120" /> <el-table-column label="塑料袋领用数量" prop="plasticBagPickQty" min-width="120" /> <el-table-column label="退料数量" prop="returnQty" min-width="110" /> <el-table-column label="实际数量" prop="actualQty" min-width="110" /> </el-table> <template #footer> <span class="dialog-footer"> <el-button type="warning" :loading="materialReturnConfirming" :disabled="!canOpenReturnSummary" @click="openReturnSummaryDialog" > 退料确认 </el-button> <!-- <el-button--> <!-- type="warning"--> <!-- :loading="materialReturnConfirming"--> <!-- :disabled="!canOpenReturnSummary"--> <!-- @click="openReturnSummaryDialog"--> <!-- >--> <!-- 退料确认--> <!-- </el-button>--> <el-button @click="dialogVisible = false">取消</el-button> </span> </template> </el-dialog> <el-dialog v-model="supplementRecordDialogVisible" title="补料记录" width="800px"> <el-table v-loading="supplementRecordLoading" :data="supplementRecordTableData" border row-key="id"> <el-table-column label="补料数量" prop="supplementQty" min-width="120" /> <el-table-column label="补料人" prop="supplementUserName" min-width="120" /> <el-table-column label="补料日期" prop="supplementTime" min-width="160" /> <el-table-column label="补料原因" prop="supplementReason" min-width="200" /> </el-table> <template #footer> <span class="dialog-footer"> <el-button @click="supplementRecordDialogVisible = false">关闭</el-button> </span> </template> </el-dialog> @@ -68,7 +50,7 @@ <script setup> import { computed, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import { listMaterialPickingDetail, listMaterialSupplementRecord, confirmMaterialReturn } from "@/api/productionManagement/productionOrder.js"; import { listMaterialPickingDetail, confirmMaterialReturn } from "@/api/productionManagement/productionOrder.js"; const props = defineProps({ modelValue: { type: Boolean, default: false }, @@ -84,24 +66,37 @@ const materialDetailLoading = ref(false); const materialDetailTableData = ref([]); const materialReturnConfirming = ref(false); const supplementRecordDialogVisible = ref(false); const supplementRecordLoading = ref(false); const supplementRecordTableData = ref([]); const returnSummaryDialogVisible = ref(false); const returnSummaryList = ref([]); const getPickQty = item => { const directPick = Number(item.pickQty ?? NaN); if (Number.isFinite(directPick)) return directPick; return ( Number(item.basePaperPickQty || 0) + Number(item.cartonPickQty || 0) + Number(item.plasticBagPickQty || 0) ); }; const calcReturnQty = item => Number(item.pickQty || 0) + Number(item.supplementQty || 0) - Number(item.actualQty || 0); const canOpenReturnSummary = computed(() => materialDetailTableData.value.some(item => calcReturnQty(item) > 0) ); getPickQty(item) - Number(item.actualQty || 0); const normalizeList = (res) => { if (Array.isArray(res?.data)) return res.data; if (Array.isArray(res?.data?.records)) return res.data.records; if (Array.isArray(res?.records)) return res.records; return []; }; const canOpenReturnSummary = computed(() => { const list = Array.isArray(materialDetailTableData.value) ? materialDetailTableData.value : []; return list.some(item => calcReturnQty(item) > 0); }); const loadDetailList = async () => { if (!props.orderRow?.id) return; materialDetailLoading.value = true; materialDetailTableData.value = []; try { const res = await listMaterialPickingDetail({ orderId: props.orderRow.id }); materialDetailTableData.value = res.data || []; const res = await listMaterialPickingDetail({ productOrderId: props.orderRow.id }); materialDetailTableData.value = normalizeList(res); } finally { materialDetailLoading.value = false; } @@ -118,19 +113,6 @@ const handleClose = () => { materialDetailTableData.value = []; }; const handleViewSupplementRecord = async row => { if (!row?.id) return; supplementRecordDialogVisible.value = true; supplementRecordLoading.value = true; supplementRecordTableData.value = []; try { const res = await listMaterialSupplementRecord({ materialDetailId: row.id }); supplementRecordTableData.value = res.data || []; } finally { supplementRecordLoading.value = false; } }; const buildReturnSummary = () => { @@ -154,7 +136,7 @@ const openReturnSummaryDialog = async () => { if (!canOpenReturnSummary.value) { ElMessage.warning("退料数量=领用数量+补料数量-实际数量,且需大于0"); ElMessage.warning("退料数量=领用数量-实际数量,且需大于0"); return; } returnSummaryList.value = buildReturnSummary(); src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -5,59 +5,61 @@ <el-button type="primary" @click="handleAddMaterialRow">新增</el-button> </div> <el-table v-loading="materialTableLoading" :data="materialTableData" border row-key="tempId"> <el-table-column label="工序名称" min-width="180"> <template #default="{ row }"> <span v-if="row.bom === true">{{ row.processName || "-" }}</span> <el-select v-else v-model="row.processName" placeholder="请选择工序" clearable filterable style="width: 100%;" @change="val => handleProcessNameChange(row, val)" > <el-option v-for="item in processOptions" :key="item.id" :label="item.name" :value="item.name" /> </el-select> </template> </el-table-column> <el-table-column label="原料名称" min-width="160"> <template #default="{ row }"> <span v-if="row.bom === true">{{ row.materialName || "-" }}</span> <el-button v-else type="primary" link @click="openMaterialProductSelect(row)"> {{ row.materialName || "选择原料" }} </el-button> </template> <el-table-column label="原料名称" min-width="160" prop="materialName"> </el-table-column> <el-table-column label="原料型号" min-width="180"> <template #default="{ row }"> {{ row.materialModel || "-" }} </template> </el-table-column> <el-table-column label="需求数量" min-width="120"> <el-table-column label="原纸需要量" min-width="120"> <template #default="{ row }"> <span v-if="row.bom === true">{{ row.requiredQty ?? "-" }}</span> {{ row.basePaperQty ?? "-" }} </template> </el-table-column> <el-table-column label="纸箱需要量" min-width="120"> <template #default="{ row }"> {{ row.cartonQty ?? "-" }} </template> </el-table-column> <el-table-column label="塑料袋数量" min-width="120"> <template #default="{ row }"> {{ row.plasticBagQty ?? "-" }} </template> </el-table-column> <!-- <el-table-column label="计量单位" width="120">--> <!-- <template #default="{ row }">--> <!-- {{ row.unit || "-" }}--> <!-- </template>--> <!-- </el-table-column>--> <el-table-column label="原纸领用数量" min-width="120"> <template #default="{ row }"> <el-input-number v-else v-model="row.requiredQty" v-model="row.basePaperPickQty" :min="0" :precision="3" :step="1" controls-position="right" style="width: 100%;" @change="val => handleRequiredQtyChange(row, val)" /> </template> </el-table-column> <el-table-column label="计量单位" width="120"> <template #default="{ row }"> {{ row.unit || "-" }} </template> </el-table-column> <el-table-column label="领用数量" min-width="120"> <el-table-column label="纸箱领用数量" min-width="120"> <template #default="{ row }"> <el-input-number v-model="row.pickQty" v-model="row.cartonPickQty" :min="0" :precision="3" :step="1" controls-position="right" style="width: 100%;" /> </template> </el-table-column> <el-table-column label="塑料袋领用数量" min-width="120"> <template #default="{ row }"> <el-input-number v-model="row.plasticBagPickQty" :min="0" :precision="3" :step="1" @@ -84,7 +86,6 @@ v-model="materialProductDialogVisible" @confirm="handleMaterialProductConfirm" single request-url="/stockInventory/rawMaterials" /> </div> </template> @@ -93,7 +94,6 @@ import { computed, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js"; import { listMaterialPickingDetail, listMaterialPickingLedger, @@ -115,7 +115,6 @@ const materialTableLoading = ref(false); const materialSaving = ref(false); const materialTableData = ref([]); const processOptions = ref([]); const currentMaterialSelectRowIndex = ref(-1); let materialTempId = 0; @@ -129,38 +128,21 @@ materialModelId: row.materialModelId, materialName: row.materialName || "", materialModel: row.materialModel || "", requiredQty: Number(row.requiredQty ?? 0), basePaperQty: Number(row.basePaperQty ?? row.requiredQty ?? 0), cartonQty: Number(row.cartonQty ?? 0), plasticBagQty: Number(row.plasticBagQty ?? 0), basePaperPickQty: Number(row.basePaperPickQty ?? row.pickQty ?? 0), cartonPickQty: Number(row.cartonPickQty ?? 0), plasticBagPickQty: Number(row.plasticBagPickQty ?? 0), unit: row.unit || "", pickQty: Number(row.pickQty ?? row.requiredQty ?? 0), }); const getProcessOptions = async () => { if (!props.orderRow?.id) return; const res = await findProductProcessRouteItemList({ orderId: props.orderRow.id }); const routeList = Array.isArray(res?.data) ? res.data : res?.data?.records || []; const processMap = new Map(); routeList.forEach(item => { const processId = item.processId; const processName = item.processName; if (!processId || !processName) return; const key = `${processId}_${processName}`; if (!processMap.has(key)) { processMap.set(key, { id: processId, name: processName, }); } }); processOptions.value = Array.from(processMap.values()); }; const loadMaterialData = async () => { if (!props.orderRow?.id) return; materialTableLoading.value = true; materialTableData.value = []; await getProcessOptions(); try { const detailRes = await listMaterialPickingDetail({ orderId: props.orderRow.id }); const detailRes = await listMaterialPickingDetail({ productOrderId: props.orderRow.id }); const detailList = Array.isArray(detailRes?.data) ? detailRes.data : detailRes?.data?.records || []; @@ -200,19 +182,6 @@ materialTableData.value.splice(index, 1); }; const handleProcessNameChange = (row, processName) => { const process = processOptions.value.find(item => item.name === processName); row.productProcessId = process?.id; }; const handleRequiredQtyChange = (row, val) => { const required = Number(val ?? 0); row.requiredQty = required; if (!row.pickQty || Number(row.pickQty) === 0) { row.pickQty = required; } }; const openMaterialProductSelect = row => { currentMaterialSelectRowIndex.value = materialTableData.value.findIndex(item => item.tempId === row.tempId); materialProductDialogVisible.value = true; @@ -237,22 +206,23 @@ return { valid: false, message: "请先新增领料数据" }; } const invalidNewRow = materialTableData.value.find( item => item.bom !== true && (!item.processName || !item.materialName) item => item.bom !== true && !item.materialName ); if (invalidNewRow) { return { valid: false, message: "新增行的工序名称和原料名称为必填项" }; return { valid: false, message: "新增行的原料名称为必填项" }; } const invalidRow = materialTableData.value.find( item => !item.processName || !item.materialName || item.requiredQty === null || item.requiredQty === undefined || item.pickQty === null || item.pickQty === undefined item.basePaperPickQty === null || item.basePaperPickQty === undefined || item.cartonPickQty === null || item.cartonPickQty === undefined || item.plasticBagPickQty === null || item.plasticBagPickQty === undefined ); if (invalidRow) { return { valid: false, message: "请完善工序、原料和数量后再保存" }; return { valid: false, message: "请完善领用数量后再保存" }; } return { valid: true, message: "" }; }; @@ -266,22 +236,7 @@ } materialSaving.value = true; try { await saveMaterialPickingLedger({ orderId: props.orderRow.id, items: materialTableData.value.map(item => ({ id: item.id, processId: item.processName, productProcessId: item.productProcessId, processName: item.processName, bom: item.bom === true, materialModelId: item.materialModelId, materialName: item.materialName, materialModel: item.materialModel, requiredQty: item.requiredQty, unit: item.unit, pickQty: item.pickQty, })), }); await saveMaterialPickingLedger(materialTableData.value[0]); emit("saved"); dialogVisible.value = false; } finally { src/views/productionManagement/productionOrder/index.vue
@@ -75,7 +75,7 @@ </div> <el-dialog v-model="bindRouteDialogVisible" title="绑定工艺路线" width="500px"> width="1280px"> <el-form label-width="90px"> <el-form-item label="工艺路线"> <el-select v-model="bindForm.routeId" @@ -87,6 +87,106 @@ :label="`${item.processRouteCode || ''}`" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="用料信息"> <el-table :data="bindRouteTableData" border size="small" class="bind-route-table"> <!-- <el-table-column label="品牌"--> <!-- min-width="90"--> <!-- align="center">--> <!-- <template #default="{ row }">--> <!-- {{ row.brand || row.productBrand || "--" }}--> <!-- </template>--> <!-- </el-table-column>--> <el-table-column label="品名" prop="productCategory" min-width="130" align="center" show-overflow-tooltip /> <el-table-column label="预定交货日期" min-width="120" align="center"> <template #default="{ row }"> {{ formatBindDate(row.deliveryDate) }} </template> </el-table-column> <el-table-column label="订单数量" prop="quantity" min-width="100" align="center" /> <el-table-column label="原纸规格" min-width="160" align="center"> <template #default> <el-tree-select v-model="bindForm.productCategoryId" placeholder="请选择原纸规格" filterable clearable check-strictly :render-after-expand="false" style="width: 100%;" :loading="bindProductLoading" :data="bindProductOptions" @change="handleBindCategoryChange" /> </template> </el-table-column> <el-table-column label="原料型号" min-width="160" align="center"> <template #default> <el-select v-model="bindForm.materialModel" placeholder="请选择原料型号" filterable clearable style="width: 100%;" :loading="bindMaterialModelLoading" > <el-option v-for="item in bindMaterialModelOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </template> </el-table-column> <el-table-column label="原纸需要量" min-width="120" align="center"> <template #default> <el-input-number v-model="bindForm.basePaperQty" :min="0" :precision="2" controls-position="right" style="width: 100%" /> </template> </el-table-column> <el-table-column label="纸箱需要量" min-width="120" align="center"> <template #default> <el-input-number v-model="bindForm.cartonQty" :min="0" :precision="2" controls-position="right" style="width: 100%" /> </template> </el-table-column> <el-table-column label="塑料袋数量" min-width="120" align="center"> <template #default> <el-input-number v-model="bindForm.plasticBagQty" :min="0" :precision="2" controls-position="right" style="width: 100%" /> </template> </el-table-column> </el-table> </el-form-item> </el-form> <template #footer> @@ -125,8 +225,9 @@ productOrderListPage, listProcessRoute, bindingRoute, listProcessBom, delProductOrder, delProductOrder, } from "@/api/productionManagement/productionOrder.js"; import { modelList, productTreeList } from "@/api/basicData/product.js"; import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js"; import MaterialLedgerDialog from "@/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue"; import MaterialDetailDialog from "@/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue"; @@ -308,18 +409,129 @@ const bindRouteLoading = ref(false); const bindRouteSaving = ref(false); const routeOptions = ref([]); const bindProductOptions = ref([]); const bindProductLoading = ref(false); const bindMaterialModelOptions = ref([]); const bindMaterialModelLoading = ref(false); const currentBindOrderRow = ref(null); const bindRouteTableData = computed(() => currentBindOrderRow.value ? [currentBindOrderRow.value] : []); const bindForm = reactive({ orderId: null, routeId: null, productCategoryId: null, productCategory: "", materialModel: "", basePaperQty: null, cartonQty: null, plasticBagQty: null, }); const materialDialogVisible = ref(false); const currentMaterialOrder = ref(null); const materialDetailDialogVisible = ref(false); const currentMaterialDetailOrder = ref(null); function convertIdToValue(data) { return (data || []).map((item) => { const { id, children, ...rest } = item; const node = { ...rest, value: id, }; if (children && children.length > 0) { node.children = convertIdToValue(children); } return node; }); } const findNodeById = (nodes, productId) => { const tree = nodes || []; for (let i = 0; i < tree.length; i++) { if (String(tree[i].value) === String(productId)) { return tree[i].label; } if (tree[i].children && tree[i].children.length > 0) { const found = findNodeById(tree[i].children, productId); if (found) return found; } } return ""; }; const loadBindProductOptions = async (row) => { bindProductLoading.value = true; try { const res = await productTreeList(); bindProductOptions.value = convertIdToValue(res || []); if (row?.productCategoryId) { bindForm.productCategoryId = row.productCategoryId; } else if (!bindForm.productCategoryId && row?.productCategory) { // 仅有名称时保留名称,不强制覆盖 bindForm.productCategory = row.productCategory; } } catch (e) { console.error("获取产品选项失败:", e); } finally { bindProductLoading.value = false; } }; const loadBindMaterialModelOptions = async (productCategoryId) => { if (!productCategoryId) { bindMaterialModelOptions.value = []; bindForm.materialModel = ""; return; } bindMaterialModelLoading.value = true; try { const res = await modelList({ id: productCategoryId }); const list = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : []); const mapped = list .map((item) => ({ label: item.model || "", value: item.model || "", })) .filter((item) => item.value); const unique = []; const seen = new Set(); mapped.forEach((item) => { if (!seen.has(item.value)) { seen.add(item.value); unique.push(item); } }); bindMaterialModelOptions.value = unique; if (!unique.some((item) => item.value === bindForm.materialModel)) { bindForm.materialModel = ""; } } catch (e) { console.error("获取原料型号失败:", e); bindMaterialModelOptions.value = []; bindForm.materialModel = ""; } finally { bindMaterialModelLoading.value = false; } }; const handleBindCategoryChange = async (value) => { bindForm.productCategoryId = value; bindForm.productCategory = findNodeById(bindProductOptions.value, value); bindForm.materialModel = ""; await loadBindMaterialModelOptions(value); }; const openBindRouteDialog = async row => { currentBindOrderRow.value = row; bindForm.orderId = row.id; bindForm.routeId = null; bindForm.productCategoryId = row.productCategoryId || null; bindForm.productCategory = row.productCategory || ""; bindForm.materialModel = row.materialModel || ""; bindForm.basePaperQty = null; bindForm.cartonQty = null; bindForm.plasticBagQty = null; bindProductOptions.value = []; bindMaterialModelOptions.value = []; bindRouteDialogVisible.value = true; routeOptions.value = []; if (!row.productModelId) { @@ -329,7 +541,7 @@ } bindRouteLoading.value = true; try { const res = await listProcessRoute({ productModelId: row.productModelId }); const res = await listProcessRoute(); routeOptions.value = res.data || []; } catch (e) { console.error("获取工艺路线列表失败:", e); @@ -337,11 +549,40 @@ } finally { bindRouteLoading.value = false; } await loadBindProductOptions(row); await loadBindMaterialModelOptions(bindForm.productCategoryId); }; const formatBindDate = value => { if (!value) return "--"; return dayjs(value).format("YYYY-MM-DD"); }; const isEmptyField = (value) => value === null || value === undefined || value === ""; const handleBindRouteConfirm = async () => { if (!bindForm.routeId) { proxy.$modal.msgWarning("请选择工艺路线"); return; } if (!bindForm.productCategoryId && !bindForm.productCategory) { proxy.$modal.msgWarning("请选择产品大类"); return; } if (!bindForm.materialModel) { proxy.$modal.msgWarning("请选择原料型号"); return; } if (isEmptyField(bindForm.basePaperQty)) { proxy.$modal.msgWarning("请填写原纸需要量"); return; } if (isEmptyField(bindForm.cartonQty)) { proxy.$modal.msgWarning("请填写纸箱需要量"); return; } if (isEmptyField(bindForm.plasticBagQty)) { proxy.$modal.msgWarning("请填写塑料袋数量"); return; } bindRouteSaving.value = true; @@ -349,6 +590,17 @@ await bindingRoute({ id: bindForm.orderId, routeId: bindForm.routeId, materialList: [ { productCategoryId: bindForm.productCategoryId, productCategory: bindForm.productCategory, materialName: bindForm.productCategory, materialModel: bindForm.materialModel, basePaperQty: bindForm.basePaperQty, cartonQty: bindForm.cartonQty, plasticBagQty: bindForm.plasticBagQty, }, ], }); proxy.$modal.msgSuccess("绑定成功"); bindRouteDialogVisible.value = false; @@ -530,4 +782,18 @@ margin-top: unset; } .bind-route-table { width: 100%; } .bind-route-table :deep(.el-table__header th) { background: #f5f7fa; font-weight: 600; } .bind-route-table :deep(.el-table td), .bind-route-table :deep(.el-table th) { padding: 8px 0; } </style> src/views/productionManagement/workOrderManagement/index.vue
@@ -271,7 +271,7 @@ }, { label: "操作", width: "260", width: "210", align: "center", dataType: "action", fixed: "right", @@ -288,12 +288,12 @@ openWorkOrderFiles(row); }, }, { name: "物料", clickFun: row => { openMaterialDialog(row); }, }, // { // name: "物料", // clickFun: row => { // openMaterialDialog(row); // }, // }, { name: "报工", clickFun: row => {