From e7303d861603e68146a45fff68c1afcc25fac10e Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期三, 02 九月 2026 00:45:34 +0800
Subject: [PATCH] fix: 生成订单领料可重复领料

---
 src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue |  346 ++++++++++++++++++++++++++++++++-------------------------
 1 files changed, 196 insertions(+), 150 deletions(-)

diff --git a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
index 6c175f9..739facb 100644
--- a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
+++ b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -2,7 +2,7 @@
   <div>
     <el-dialog v-model="dialogVisible"
                title="棰嗘枡鍙拌处"
-               width="1350px"
+               width="1450px"
                @close="handleClose">
       <div class="material-toolbar">
         <el-button type="primary"
@@ -13,7 +13,7 @@
                 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
@@ -43,13 +43,13 @@
           </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"
@@ -66,7 +66,7 @@
           </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
@@ -78,6 +78,20 @@
                              :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="璁¢噺鍗曚綅"
@@ -102,7 +116,7 @@
                          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>
@@ -130,18 +144,12 @@
   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 },
@@ -160,62 +168,61 @@
   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,
     };
   };
@@ -237,37 +244,112 @@
       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));
+        // 鎵嬪伐鏂板琛岋紙闈濨OM锛夌殑闇�姹傛暟閲忎互棰嗘枡蹇収涓哄噯
+        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;
     }
@@ -281,14 +363,6 @@
       }
     }
   );
-
-  const stripTrailingZeros = val => {
-    const str = String(val);
-    if (str.includes(".")) {
-      return parseFloat(str).toString();
-    }
-    return str;
-  };
 
   const handleClose = () => {
     materialTableData.value = [];
@@ -311,11 +385,12 @@
   };
 
   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 => {
@@ -326,8 +401,6 @@
   };
 
   const handleMaterialProductConfirm = products => {
-    console.log(products, "products");
-
     if (!products || products.length === 0) return;
     const index = currentMaterialSelectRowIndex.value;
     if (index < 0 || !materialTableData.value[index]) return;
@@ -340,7 +413,7 @@
     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;
   };
@@ -349,33 +422,22 @@
     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: "" };
   };
@@ -389,47 +451,26 @@
     }
     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");
@@ -445,4 +486,9 @@
     margin-bottom: 12px;
     text-align: right;
   }
+
+  .remaining-empty {
+    color: #f56c6c;
+    font-weight: 600;
+  }
 </style>

--
Gitblit v1.9.3