| | |
| | | <div> |
| | | <el-dialog v-model="dialogVisible" |
| | | title="领料台账" |
| | | width="1350px" |
| | | width="1450px" |
| | | @close="handleClose"> |
| | | <div class="material-toolbar"> |
| | | <el-button type="primary" |
| | |
| | | border |
| | | row-key="tempId"> |
| | | <el-table-column label="工序名称" |
| | | min-width="80"> |
| | | min-width="100"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.bom === true">{{ row.operationName || "-" }}</span> |
| | | <el-select v-else |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="原料型号" |
| | | min-width="120"> |
| | | min-width="110"> |
| | | <template #default="{ row }"> |
| | | {{ row.materialModel || "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <!-- 批号多选 --> |
| | | <el-table-column min-width="200" |
| | | <el-table-column min-width="180" |
| | | label="批号"> |
| | | <template #default="{ row }"> |
| | | <el-select v-model="row.batchNo" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="需求数量" |
| | | min-width="100"> |
| | | min-width="95"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.bom === true">{{ stripTrailingZeros(row.demandedQuantity) ?? "-" }}</span> |
| | | <el-input-number v-else |
| | |
| | | :parser="value => parseFloat(value) || 0" |
| | | style="width: 100%;" |
| | | @change="val => handleRequiredQtyChange(row, val)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="已领数量" |
| | | min-width="90"> |
| | | <template #default="{ row }"> |
| | | {{ stripTrailingZeros(row.pickedQty) ?? 0 }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="剩余可领" |
| | | min-width="90"> |
| | | <template #default="{ row }"> |
| | | <span :class="{ 'remaining-empty': Number(row.remainingQty) <= 0 }"> |
| | | {{ stripTrailingZeros(row.remainingQty) ?? 0 }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计量单位" |
| | |
| | | width="90" |
| | | fixed="right"> |
| | | <template #default="{ $index, row }"> |
| | | <el-button v-if="row.bom !== true" |
| | | <el-button v-if="row.bom !== true || Number(row.pickQty) > 0" |
| | | type="danger" |
| | | link |
| | | @click="handleDeleteMaterialRow($index)">删除</el-button> |
| | |
| | | import { computed, ref, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | import { |
| | | findProductProcessRouteItemList, |
| | | listMain, |
| | | } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { |
| | | listMaterialPickingDetail, |
| | | listMaterialPickingBom, |
| | | listMaterialPickingLedger, |
| | | saveMaterialPickingLedger, |
| | | updateMaterialPickingLedger, |
| | | } from "@/api/productionManagement/productionOrder.js"; |
| | | import { queryList2 } from "@/api/productionManagement/productStructure.js"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | |
| | | const materialTableData = ref([]); |
| | | |
| | | const isSaveDisabled = computed(() => { |
| | | if (materialTableLoading.value) return true; |
| | | if (materialTableData.value.length === 0) return true; |
| | | return !materialTableData.value.some(row => { |
| | | // 检查是否有任何用户输入内容 |
| | | const hasBatch = Array.isArray(row.batchNo) && row.batchNo.length > 0; |
| | | const hasPickQty = |
| | | row.pickQty !== null && row.pickQty !== undefined && row.pickQty !== 0; |
| | | |
| | | if (row.bom) { |
| | | // 对于来自BOM的行,输入框只有“批号”和“领用数量” |
| | | return hasBatch || hasPickQty; |
| | | } else { |
| | | // 对于新增行,输入框包括“工序”、“原料”、“需求数量”、“批号”和“领用数量” |
| | | const hasOperation = !!row.operationName; |
| | | const hasMaterial = !!row.materialName; |
| | | const hasDemanded = |
| | | row.demandedQuantity !== null && |
| | | row.demandedQuantity !== undefined && |
| | | row.demandedQuantity !== 0; |
| | | return ( |
| | | hasBatch || hasPickQty || hasOperation || hasMaterial || hasDemanded |
| | | ); |
| | | } |
| | | }); |
| | | // 只有存在本次领用数量大于0的行才允许保存 |
| | | return !materialTableData.value.some(item => Number(item.pickQty) > 0); |
| | | }); |
| | | |
| | | const processOptions = ref([]); |
| | | const currentMaterialSelectRowIndex = ref(-1); |
| | | let materialTempId = 0; |
| | | |
| | | const getDefaultBatchNo = batchNoList => { |
| | | if (!Array.isArray(batchNoList) || batchNoList.length === 0) return []; |
| | | return [batchNoList[0]]; |
| | | const toNumber = val => { |
| | | const n = Number(val); |
| | | return Number.isFinite(n) ? n : 0; |
| | | }; |
| | | |
| | | // 保留6位小数,避免浮点误差 |
| | | const roundQuantity = val => Math.round(toNumber(val) * 1e6) / 1e6; |
| | | |
| | | // 需求数量分组键:与后端保持一致(工序ID优先,缺失时用工序名称) |
| | | const buildDemandKey = (technologyOperationId, operationName, productModelId) => |
| | | `${technologyOperationId != null && technologyOperationId !== undefined ? "T" + technologyOperationId : "N" + (operationName || "")}#${productModelId ?? ""}`; |
| | | |
| | | const stripTrailingZeros = val => { |
| | | if (val === null || val === undefined || val === "") return "0"; |
| | | const str = String(val); |
| | | if (str.includes(".")) { |
| | | return parseFloat(str).toString(); |
| | | } |
| | | return str; |
| | | }; |
| | | |
| | | const createMaterialRow = (row = {}) => { |
| | | const batchNoList = Array.isArray(row.batchNoList) ? row.batchNoList : []; |
| | | const batchNo = row.batchNo |
| | | ? typeof row.batchNo === "string" |
| | | ? row.batchNo.split(",") |
| | | : row.batchNo |
| | | : getDefaultBatchNo(batchNoList); |
| | | const pickedEntry = row.pickedEntry || null; |
| | | const pickedQty = pickedEntry ? roundQuantity(pickedEntry.pickedQty) : 0; |
| | | const returnedQty = pickedEntry ? roundQuantity(pickedEntry.returnedQty) : 0; |
| | | const demandedQuantity = roundQuantity(row.demandedQuantity ?? 0); |
| | | const remainingQty = Math.max(roundQuantity(demandedQuantity - pickedQty + returnedQty), 0); |
| | | |
| | | return { |
| | | tempId: row.id || `temp_${++materialTempId}`, |
| | | id: row.id, |
| | | processId: row.processId || row.technologyOperationId, |
| | | technologyOperationId: row.technologyOperationId || row.processId, |
| | | operationName: row.operationName || "", |
| | | tempId: `temp_${++materialTempId}`, |
| | | id: null, |
| | | bom: row.bom === true, |
| | | materialModelId: row.materialModelId || row.productModelId, |
| | | materialName: row.materialName || row.productName || "", |
| | | materialModel: row.materialModel || row.model || "", |
| | | demandedQuantity: Number(row.requiredQty ?? row.demandedQuantity ?? 0), |
| | | technologyOperationId: row.technologyOperationId ?? null, |
| | | operationName: row.operationName || "", |
| | | materialModelId: row.materialModelId ?? null, |
| | | materialName: row.materialName || "", |
| | | materialModel: row.materialModel || "", |
| | | unit: row.unit || "", |
| | | pickQty: Number(row.pickQty ?? row.pickQuantity ?? row.requiredQty ?? row.demandedQuantity ?? 0), |
| | | batchNo, |
| | | demandedQuantity, |
| | | pickedQty, |
| | | returnedQty, |
| | | remainingQty, |
| | | pickQty: 0, |
| | | batchNo: [], |
| | | batchNoList, |
| | | }; |
| | | }; |
| | |
| | | if (!processMap.has(key)) { |
| | | processMap.set(key, { |
| | | id: processId, |
| | | technologyOperationId: processId, |
| | | name: operationName, |
| | | }); |
| | | } |
| | | }); |
| | | processOptions.value = Array.from(processMap.values()); |
| | | }; |
| | | const isDetail = ref(true); |
| | | |
| | | const loadMaterialData = async () => { |
| | | if (!props.orderRow?.id) return; |
| | | materialTableLoading.value = true; |
| | | materialTableData.value = []; |
| | | await getProcessOptions(); |
| | | try { |
| | | const detailRes = await listMaterialPickingDetail(props.orderRow.id); |
| | | await getProcessOptions(); |
| | | // 同时获取BOM需求数据与已领料明细,用于计算“已领数量/剩余可领” |
| | | const [bomRes, detailRes] = await Promise.all([ |
| | | listMaterialPickingBom(props.orderRow.id), |
| | | listMaterialPickingDetail(props.orderRow.id), |
| | | ]); |
| | | const bomList = Array.isArray(bomRes?.data) |
| | | ? bomRes.data |
| | | : bomRes?.data?.records || []; |
| | | const detailList = Array.isArray(detailRes?.data) |
| | | ? detailRes.data |
| | | : detailRes?.data?.records || []; |
| | | if (detailList.length > 0) { |
| | | isDetail.value = true; |
| | | materialTableData.value = detailList.map(item => createMaterialRow(item)); |
| | | return; |
| | | } else { |
| | | isDetail.value = false; |
| | | const bomRes = await listMaterialPickingBom(props.orderRow.id); |
| | | const bomList = Array.isArray(bomRes?.data) |
| | | ? bomRes.data |
| | | : bomRes?.data?.records || []; |
| | | materialTableData.value = bomList.map(item => createMaterialRow(item)); |
| | | return; |
| | | } |
| | | |
| | | // 按“工序+原料”汇总历史领料:已领数量、退料数量 |
| | | const pickedMap = new Map(); |
| | | detailList.forEach(item => { |
| | | if (!item) return; |
| | | const key = buildDemandKey( |
| | | item.technologyOperationId, |
| | | item.operationName, |
| | | item.productModelId |
| | | ); |
| | | const entry = pickedMap.get(key) || { |
| | | pickedQty: 0, |
| | | returnedQty: 0, |
| | | manualDemand: 0, |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName || "", |
| | | materialModelId: item.productModelId, |
| | | materialName: item.productName || "", |
| | | materialModel: item.model || "", |
| | | unit: item.unit || "", |
| | | batchNoList: Array.isArray(item.batchNoList) |
| | | ? item.batchNoList |
| | | : [], |
| | | }; |
| | | entry.pickedQty = roundQuantity(entry.pickedQty + toNumber(item.pickQuantity)); |
| | | entry.returnedQty = roundQuantity(entry.returnedQty + toNumber(item.returnQty)); |
| | | // 手工新增行(非BOM)的需求数量以领料快照为准 |
| | | if (item.bom !== true && item.demandedQuantity != null) { |
| | | entry.manualDemand = Math.max( |
| | | toNumber(entry.manualDemand), |
| | | toNumber(item.demandedQuantity) |
| | | ); |
| | | } |
| | | pickedMap.set(key, entry); |
| | | }); |
| | | |
| | | const rows = []; |
| | | const bomKeySet = new Set(); |
| | | // BOM行:需求数量取BOM,已领数量取历史领料汇总 |
| | | bomList.forEach(item => { |
| | | const materialModelId = item.productModelId ?? item.materialModelId; |
| | | const key = buildDemandKey( |
| | | item.technologyOperationId, |
| | | item.operationName, |
| | | materialModelId |
| | | ); |
| | | bomKeySet.add(key); |
| | | rows.push( |
| | | createMaterialRow({ |
| | | bom: true, |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName || "", |
| | | materialModelId, |
| | | materialName: item.productName || item.materialName || "", |
| | | materialModel: item.model || item.materialModel || "", |
| | | unit: item.unit || "", |
| | | demandedQuantity: item.demandedQuantity ?? item.requiredQty ?? 0, |
| | | pickedEntry: pickedMap.get(key), |
| | | batchNoList: item.batchNoList || [], |
| | | }) |
| | | ); |
| | | }); |
| | | // 不在BOM中的历史领料(手工新增行),也展示出来支持继续领料 |
| | | pickedMap.forEach((entry, key) => { |
| | | if (bomKeySet.has(key)) return; |
| | | rows.push( |
| | | createMaterialRow({ |
| | | bom: false, |
| | | technologyOperationId: entry.technologyOperationId, |
| | | operationName: entry.operationName, |
| | | materialModelId: entry.materialModelId, |
| | | materialName: entry.materialName, |
| | | materialModel: entry.materialModel, |
| | | unit: entry.unit, |
| | | demandedQuantity: entry.manualDemand, |
| | | pickedEntry: entry, |
| | | batchNoList: entry.batchNoList, |
| | | }) |
| | | ); |
| | | }); |
| | | materialTableData.value = rows; |
| | | } finally { |
| | | materialTableLoading.value = false; |
| | | } |
| | |
| | | } |
| | | } |
| | | ); |
| | | |
| | | const stripTrailingZeros = val => { |
| | | const str = String(val); |
| | | if (str.includes(".")) { |
| | | return parseFloat(str).toString(); |
| | | } |
| | | return str; |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | | materialTableData.value = []; |
| | |
| | | }; |
| | | |
| | | const handleRequiredQtyChange = (row, val) => { |
| | | const required = Number(val ?? 0); |
| | | const required = roundQuantity(val); |
| | | row.demandedQuantity = required; |
| | | if (!row.pickQty || Number(row.pickQty) === 0) { |
| | | row.pickQty = required; |
| | | } |
| | | row.remainingQty = Math.max( |
| | | roundQuantity(required - toNumber(row.pickedQty) + toNumber(row.returnedQty)), |
| | | 0 |
| | | ); |
| | | }; |
| | | |
| | | const openMaterialProductSelect = row => { |
| | |
| | | }; |
| | | |
| | | const handleMaterialProductConfirm = products => { |
| | | console.log(products, "products"); |
| | | |
| | | if (!products || products.length === 0) return; |
| | | const index = currentMaterialSelectRowIndex.value; |
| | | if (index < 0 || !materialTableData.value[index]) return; |
| | |
| | | row.materialModel = product.materialModel || product.model || ""; |
| | | row.unit = product.unit || product.measureUnit || ""; |
| | | row.batchNoList = Array.isArray(product.batchNoList) ? product.batchNoList : []; |
| | | row.batchNo = getDefaultBatchNo(row.batchNoList); |
| | | row.batchNo = []; |
| | | currentMaterialSelectRowIndex.value = -1; |
| | | materialProductDialogVisible.value = false; |
| | | }; |
| | |
| | | if (materialTableData.value.length === 0) { |
| | | return { valid: false, message: "请先新增领料数据" }; |
| | | } |
| | | const invalidNewRow = materialTableData.value.find( |
| | | item => item.bom !== true && (!item.operationName || !item.materialName) |
| | | const pickRows = materialTableData.value.filter( |
| | | item => toNumber(item.pickQty) > 0 |
| | | ); |
| | | if (invalidNewRow) { |
| | | return { valid: false, message: "新增行的工序名称和原料名称为必填项" }; |
| | | if (pickRows.length === 0) { |
| | | return { valid: false, message: "请至少填写一行大于0的领用数量" }; |
| | | } |
| | | const invalidRow = materialTableData.value.find( |
| | | item => |
| | | !item.operationName || |
| | | !item.materialName || |
| | | (Number(item.pickQty) > 0 && |
| | | (!item.batchNo || item.batchNo.length === 0)) || |
| | | (item.batchNo && item.batchNo.length > 0 && Number(item.pickQty) <= 0) || |
| | | item.demandedQuantity === null || |
| | | item.demandedQuantity === undefined || |
| | | item.pickQty === null || |
| | | item.pickQty === undefined |
| | | ); |
| | | if (invalidRow) { |
| | | if ( |
| | | invalidRow.batchNo && |
| | | invalidRow.batchNo.length > 0 && |
| | | Number(invalidRow.pickQty) <= 0 |
| | | ) { |
| | | return { valid: false, message: "选择了批号时,领用数量必须大于零" }; |
| | | for (let i = 0; i < materialTableData.value.length; i++) { |
| | | const item = materialTableData.value[i]; |
| | | const rowNo = i + 1; |
| | | if (toNumber(item.pickQty) <= 0) continue; |
| | | if (item.bom !== true && (!item.operationName || !item.materialName)) { |
| | | return { valid: false, message: `第${rowNo}行的工序名称和原料名称为必填项` }; |
| | | } |
| | | return { valid: false, message: "请完善工序、原料、批号和数量后再保存" }; |
| | | if (!Array.isArray(item.batchNo) || item.batchNo.length === 0) { |
| | | return { valid: false, message: `第${rowNo}行请选择批号` }; |
| | | } |
| | | } |
| | | return { valid: true, message: "" }; |
| | | }; |
| | |
| | | } |
| | | materialSaving.value = true; |
| | | try { |
| | | if (isDetail.value) { |
| | | await updateMaterialPickingLedger({ |
| | | productionOrderId: props.orderRow.id, |
| | | productionOrderPickDto: materialTableData.value.map(item => ({ |
| | | id: item.id, |
| | | // processId: item.operationName, |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName, |
| | | bom: item.bom === true, |
| | | productModelId: item.materialModelId, |
| | | // materialName: item.materialName, |
| | | // materialModel: item.materialModel, |
| | | demandedQuantity: item.demandedQuantity, |
| | | unit: item.unit, |
| | | pickQuantity: item.pickQty, |
| | | batchNo: Array.isArray(item.batchNo) |
| | | ? item.batchNo.join(",") |
| | | : item.batchNo, |
| | | })), |
| | | }); |
| | | } else { |
| | | await saveMaterialPickingLedger({ |
| | | productionOrderId: props.orderRow.id, |
| | | productionOrderPickDto: materialTableData.value.map(item => ({ |
| | | id: item.id, |
| | | // processId: item.operationName, |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName, |
| | | bom: item.bom === true, |
| | | productModelId: item.materialModelId, |
| | | // materialName: item.materialName, |
| | | // materialModel: item.materialModel, |
| | | demandedQuantity: item.demandedQuantity, |
| | | unit: item.unit, |
| | | pickQuantity: item.pickQty, |
| | | batchNo: Array.isArray(item.batchNo) |
| | | ? item.batchNo.join(",") |
| | | : item.batchNo, |
| | | })), |
| | | }); |
| | | } |
| | | // 每次保存都是一次独立的领料:只提交本次领用数量大于0的行, |
| | | // 出库台账按本次领料数量记账,历史领料记录保持不变。 |
| | | const pickRows = materialTableData.value.filter( |
| | | item => toNumber(item.pickQty) > 0 |
| | | ); |
| | | await saveMaterialPickingLedger({ |
| | | productionOrderId: props.orderRow.id, |
| | | productionOrderPickDto: pickRows.map(item => ({ |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName, |
| | | bom: item.bom === true, |
| | | productModelId: item.materialModelId, |
| | | demandedQuantity: item.demandedQuantity, |
| | | unit: item.unit, |
| | | pickQuantity: toNumber(item.pickQty), |
| | | batchNo: Array.isArray(item.batchNo) |
| | | ? item.batchNo.join(",") |
| | | : item.batchNo, |
| | | })), |
| | | }); |
| | | |
| | | ElMessage({ message: "领料成功", type: "success" }); |
| | | emit("saved"); |
| | |
| | | margin-bottom: 12px; |
| | | text-align: right; |
| | | } |
| | | |
| | | .remaining-empty { |
| | | color: #f56c6c; |
| | | font-weight: 600; |
| | | } |
| | | </style> |