From 9840e565b021ca4ed199ea40d2c8b47bc9feeb3f Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 03 九月 2026 14:50:49 +0800
Subject: [PATCH] fix: 金额优化

---
 src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue |  380 ++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 297 insertions(+), 83 deletions(-)

diff --git a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
index 6b040d6..ca25853 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="1200px"
+               width="1450px"
                @close="handleClose">
       <div class="material-toolbar">
         <el-button type="primary"
@@ -13,25 +13,25 @@
                 border
                 row-key="tempId">
         <el-table-column label="宸ュ簭鍚嶇О"
-                         min-width="180">
+                         min-width="100">
           <template #default="{ row }">
-            <span v-if="row.bom === true">{{ row.processName || "-" }}</span>
+            <span v-if="row.bom === true">{{ row.operationName || "-" }}</span>
             <el-select v-else
-                       v-model="row.processName"
+                       v-model="row.operationName"
                        placeholder="璇烽�夋嫨宸ュ簭"
                        clearable
                        filterable
                        style="width: 100%;"
                        @change="val => handleProcessNameChange(row, val)">
               <el-option v-for="item in processOptions"
-                         :key="item.id"
+                         :key="item.technologyOperationId"
                          :label="item.name"
                          :value="item.name" />
             </el-select>
           </template>
         </el-table-column>
         <el-table-column label="鍘熸枡鍚嶇О"
-                         min-width="160">
+                         min-width="120">
           <template #default="{ row }">
             <span v-if="row.bom === true">{{ row.materialName || "-" }}</span>
             <el-button v-else
@@ -43,27 +43,66 @@
           </template>
         </el-table-column>
         <el-table-column label="鍘熸枡鍨嬪彿"
-                         min-width="180">
+                         min-width="110">
           <template #default="{ row }">
             {{ row.materialModel || "-" }}
           </template>
         </el-table-column>
-        <el-table-column label="闇�姹傛暟閲�"
-                         min-width="120">
+        <!-- 鎵瑰彿澶氶�� -->
+        <el-table-column min-width="180"
+                         label="鎵瑰彿">
           <template #default="{ row }">
-            <span v-if="row.bom === true">{{ row.requiredQty ?? "-" }}</span>
+            <el-select v-model="row.batchNo"
+                       multiple
+                       collapse-tags
+                       collapse-tags-indicator
+                       placeholder="璇烽�夋嫨鎵瑰彿"
+                       style="width: 100%;">
+              <el-option v-for="item in row.batchNoList"
+                         :key="item"
+                         :label="item"
+                         :value="item" />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="闇�姹傛暟閲�"
+                         min-width="95">
+          <template #default="{ row }">
+            <span v-if="row.bom === true">{{ stripTrailingZeros(row.demandedQuantity) ?? "-" }}</span>
             <el-input-number v-else
-                             v-model="row.requiredQty"
+                             v-model="row.demandedQuantity"
                              :min="0"
-                             :precision="3"
                              :step="1"
                              controls-position="right"
+                             :formatter="value => stripTrailingZeros(value)"
+                             :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 }">
+            <span v-if="row.stockQty === null">-</span>
+            <span v-else>{{ stripTrailingZeros(row.stockQty) }}</span>
+          </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="120">
+                         width="80">
           <template #default="{ row }">
             {{ row.unit || "-" }}
           </template>
@@ -73,9 +112,10 @@
           <template #default="{ row }">
             <el-input-number v-model="row.pickQty"
                              :min="0"
-                             :precision="3"
                              :step="1"
                              controls-position="right"
+                             :formatter="value => stripTrailingZeros(value)"
+                             :parser="value => parseFloat(value) || 0"
                              style="width: 100%;" />
           </template>
         </el-table-column>
@@ -83,7 +123,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>
@@ -94,6 +134,7 @@
         <span class="dialog-footer">
           <el-button type="primary"
                      :loading="materialSaving"
+                     :disabled="isSaveDisabled"
                      @click="handleMaterialSave">淇濆瓨</el-button>
           <el-button @click="dialogVisible = false">鍙栨秷</el-button>
         </span>
@@ -111,9 +152,10 @@
   import { ElMessage } from "element-plus";
   import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
   import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js";
+  import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
   import {
     listMaterialPickingDetail,
-    listMaterialPickingLedger,
+    listMaterialPickingBom,
     saveMaterialPickingLedger,
   } from "@/api/productionManagement/productionOrder.js";
 
@@ -132,24 +174,67 @@
   const materialTableLoading = ref(false);
   const materialSaving = ref(false);
   const materialTableData = ref([]);
+
+  const isSaveDisabled = computed(() => {
+    if (materialTableLoading.value) return true;
+    if (materialTableData.value.length === 0) return true;
+    // 鍙湁瀛樺湪鏈棰嗙敤鏁伴噺澶т簬0鐨勮鎵嶅厑璁镐繚瀛�
+    return !materialTableData.value.some(item => Number(item.pickQty) > 0);
+  });
+
   const processOptions = ref([]);
   const currentMaterialSelectRowIndex = ref(-1);
   let materialTempId = 0;
 
-  const createMaterialRow = (row = {}) => ({
-    tempId: row.id || `temp_${++materialTempId}`,
-    id: row.id,
-    processId: row.processId,
-    productProcessId: row.productProcessId || row.processId,
-    processName: row.processName || "",
-    bom: row.bom === true,
-    materialModelId: row.materialModelId,
-    materialName: row.materialName || "",
-    materialModel: row.materialModel || "",
-    requiredQty: Number(row.requiredQty ?? 0),
-    unit: row.unit || "",
-    pickQty: Number(row.pickQty ?? row.requiredQty ?? 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 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: `temp_${++materialTempId}`,
+      id: null,
+      bom: row.bom === true,
+      technologyOperationId: row.technologyOperationId ?? null,
+      operationName: row.operationName || "",
+      materialModelId: row.materialModelId ?? null,
+      materialName: row.materialName || "",
+      materialModel: row.materialModel || "",
+      unit: row.unit || "",
+      demandedQuantity,
+      pickedQty,
+      returnedQty,
+      remainingQty,
+      pickQty: 0,
+      stockQty: null,
+      batchNo: [],
+      batchNoList,
+    };
+  };
 
   const getProcessOptions = async () => {
     if (!props.orderRow?.id) return;
@@ -161,14 +246,15 @@
       : 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}`;
+      const processId = item.technologyOperationId;
+      const operationName = item.operationName;
+      if (!processId || !operationName) return;
+      const key = `${processId}_${operationName}`;
       if (!processMap.has(key)) {
         processMap.set(key, {
           id: processId,
-          name: processName,
+          technologyOperationId: processId,
+          name: operationName,
         });
       }
     });
@@ -179,25 +265,103 @@
     if (!props.orderRow?.id) return;
     materialTableLoading.value = true;
     materialTableData.value = [];
-    await getProcessOptions();
     try {
-      const detailRes = await listMaterialPickingDetail({
-        orderId: 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) {
-        materialTableData.value = detailList.map(item => createMaterialRow(item));
-        return;
-      }
-      const ledgerRes = await listMaterialPickingLedger({
-        orderId: props.orderRow.id,
+
+      // 鎸夆�滃伐搴�+鍘熸枡鈥濇眹鎬诲巻鍙查鏂欙細宸查鏁伴噺銆侀��鏂欐暟閲�
+      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 ledgerList = Array.isArray(ledgerRes?.data)
-        ? ledgerRes.data
-        : ledgerRes?.data?.records || [];
-      materialTableData.value = ledgerList.map(item => createMaterialRow(item));
+
+      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,
+          })
+        );
+      });
+      // 鍏堟煡鍑哄簱瀛樹綑閲忓啀鍏ヨ〃锛屼繚璇侀娆℃墦寮�鍗冲睍绀猴紙rows 灏氭湭杞负鍝嶅簲寮忎唬鐞嗭紝
+      // 鍏ヨ〃鍚庡啀鏀瑰師濮嬪璞′笉浼氳Е鍙戣鍥炬洿鏂帮級
+      await loadStockQtyForRows(rows);
+      materialTableData.value = rows;
     } finally {
       materialTableLoading.value = false;
     }
@@ -217,6 +381,40 @@
     currentMaterialSelectRowIndex.value = -1;
   };
 
+  // 鎸夊瀷鍙锋眹鎬绘墍鏈夋壒娆$殑鍘熸枡鍙敤搴撳瓨浣欓噺
+  const loadStockQtyForRows = async rows => {
+    const modelIds = [
+      ...new Set(rows.map(row => row.materialModelId).filter(Boolean)),
+    ];
+    const stockMap = new Map();
+    await Promise.all(
+      modelIds.map(async modelId => {
+        try {
+          const res = await getStockInventoryByModelId(modelId);
+          const list = Array.isArray(res?.data)
+            ? res.data
+            : res?.data?.records || res?.data?.rows || [];
+          const total = list.reduce(
+            (sum, item) => sum + toNumber(item?.qualitity ?? item?.quantity),
+            0
+          );
+          stockMap.set(modelId, roundQuantity(total));
+        } catch (e) {
+          stockMap.set(modelId, 0);
+        }
+      })
+    );
+    rows.forEach(row => {
+      if (!row.materialModelId) {
+        row.stockQty = null;
+        return;
+      }
+      row.stockQty = stockMap.has(row.materialModelId)
+        ? stockMap.get(row.materialModelId)
+        : 0;
+    });
+  };
+
   const handleAddMaterialRow = () => {
     materialTableData.value.push(createMaterialRow());
   };
@@ -225,17 +423,20 @@
     materialTableData.value.splice(index, 1);
   };
 
-  const handleProcessNameChange = (row, processName) => {
-    const process = processOptions.value.find(item => item.name === processName);
-    row.productProcessId = process?.id;
+  const handleProcessNameChange = (row, operationName) => {
+    const process = processOptions.value.find(
+      item => item.name === operationName
+    );
+    row.technologyOperationId = process?.technologyOperationId;
   };
 
   const handleRequiredQtyChange = (row, val) => {
-    const required = Number(val ?? 0);
-    row.requiredQty = required;
-    if (!row.pickQty || Number(row.pickQty) === 0) {
-      row.pickQty = required;
-    }
+    const required = roundQuantity(val);
+    row.demandedQuantity = required;
+    row.remainingQty = Math.max(
+      roundQuantity(required - toNumber(row.pickedQty) + toNumber(row.returnedQty)),
+      0
+    );
   };
 
   const openMaterialProductSelect = row => {
@@ -257,31 +458,33 @@
       product.materialName || product.productName || product.name || "";
     row.materialModel = product.materialModel || product.model || "";
     row.unit = product.unit || product.measureUnit || "";
+    row.batchNoList = Array.isArray(product.batchNoList) ? product.batchNoList : [];
+    row.batchNo = [];
     currentMaterialSelectRowIndex.value = -1;
     materialProductDialogVisible.value = false;
+    loadStockQtyForRows([row]);
   };
 
   const validateMaterialRows = () => {
     if (materialTableData.value.length === 0) {
       return { valid: false, message: "璇峰厛鏂板棰嗘枡鏁版嵁" };
     }
-    const invalidNewRow = materialTableData.value.find(
-      item => item.bom !== true && (!item.processName || !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.processName ||
-        !item.materialName ||
-        item.requiredQty === null ||
-        item.requiredQty === undefined ||
-        item.pickQty === null ||
-        item.pickQty === undefined
-    );
-    if (invalidRow) {
-      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}琛岀殑宸ュ簭鍚嶇О鍜屽師鏂欏悕绉颁负蹇呭~椤筦 };
+      }
+      if (!Array.isArray(item.batchNo) || item.batchNo.length === 0) {
+        return { valid: false, message: `绗�${rowNo}琛岃閫夋嫨鎵瑰彿` };
+      }
     }
     return { valid: true, message: "" };
   };
@@ -295,22 +498,28 @@
     }
     materialSaving.value = true;
     try {
+      // 姣忔淇濆瓨閮芥槸涓�娆$嫭绔嬬殑棰嗘枡锛氬彧鎻愪氦鏈棰嗙敤鏁伴噺澶т簬0鐨勮锛�
+      // 鍑哄簱鍙拌处鎸夋湰娆¢鏂欐暟閲忚璐︼紝鍘嗗彶棰嗘枡璁板綍淇濇寔涓嶅彉銆�
+      const pickRows = materialTableData.value.filter(
+        item => toNumber(item.pickQty) > 0
+      );
       await saveMaterialPickingLedger({
-        orderId: props.orderRow.id,
-        items: materialTableData.value.map(item => ({
-          id: item.id,
-          processId: item.processName,
-          productProcessId: item.productProcessId,
-          processName: item.processName,
+        productionOrderId: props.orderRow.id,
+        productionOrderPickDto: pickRows.map(item => ({
+          technologyOperationId: item.technologyOperationId,
+          operationName: item.operationName,
           bom: item.bom === true,
-          materialModelId: item.materialModelId,
-          materialName: item.materialName,
-          materialModel: item.materialModel,
-          requiredQty: item.requiredQty,
+          productModelId: item.materialModelId,
+          demandedQuantity: item.demandedQuantity,
           unit: item.unit,
-          pickQty: item.pickQty,
+          pickQuantity: toNumber(item.pickQty),
+          batchNo: Array.isArray(item.batchNo)
+            ? item.batchNo.join(",")
+            : item.batchNo,
         })),
       });
+
+      ElMessage({ message: "棰嗘枡鎴愬姛", type: "success" });
       emit("saved");
       dialogVisible.value = false;
     } finally {
@@ -324,4 +533,9 @@
     margin-bottom: 12px;
     text-align: right;
   }
+
+  .remaining-empty {
+    color: #f56c6c;
+    font-weight: 600;
+  }
 </style>

--
Gitblit v1.9.3