Explorar o código

fix:拆合批相关

mlchen hai 4 días
pai
achega
a252059235

+ 135 - 48
src/views/warehouseManagement/stockLedger/components/BatchChangeDialog.vue

@@ -88,39 +88,36 @@
           </el-table-column>
           <el-table-column label="计量数量">
             <template slot-scope="{ row }">
-              <el-input-number
+              <el-input
                 v-model="row.target.measureQuantity"
-                :min="hasPacking ? Number(row.packingMeasureQuantity) : 0"
-                :precision="6"
-                :step="0.000001"
+                maxlength="19"
                 :disabled="Boolean(pendingApproveId)"
                 size="mini"
+                placeholder="请输入"
               />
             </template>
           </el-table-column>
           <el-table-column label="包装数量">
             <template slot-scope="{ row }">
               <span v-if="hasPacking">{{ row.packageQuantity }}</span>
-              <el-input-number
+              <el-input
                 v-else
                 v-model="row.target.packageQuantity"
-                :min="0"
-                :precision="6"
-                :step="0.000001"
+                maxlength="19"
                 :disabled="Boolean(pendingApproveId)"
                 size="mini"
+                placeholder="请输入"
               />
             </template>
           </el-table-column>
           <el-table-column label="重量">
             <template slot-scope="{ row }">
-              <el-input-number
+              <el-input
                 v-model="row.target.weight"
-                :min="hasPacking ? Number(row.packingWeight) : 0"
-                :precision="6"
-                :step="0.000001"
+                maxlength="19"
                 :disabled="Boolean(pendingApproveId)"
                 size="mini"
+                placeholder="请输入"
               />
             </template>
           </el-table-column>
@@ -170,7 +167,7 @@
                 size="mini"
                 :disabled="mode === 'merge' || Boolean(pendingApproveId)"
                 placeholder="请选择"
-                @change="syncLockTargets"
+                @change="assignPackingTarget(row, $event)"
               >
                 <el-option
                   v-for="target in targets"
@@ -202,12 +199,9 @@
               <el-select
                 v-model="row.targetDetailId"
                 size="mini"
-                :disabled="
-                  mode === 'merge' ||
-                  row.lockedPackingStockIds.length > 0 ||
-                  Boolean(pendingApproveId)
-                "
+                :disabled="mode === 'merge' || Boolean(pendingApproveId)"
                 placeholder="请选择"
+                @change="assignLockOrderTarget(row, $event)"
               >
                 <el-option
                   v-for="target in targets"
@@ -328,6 +322,24 @@
     return decimal(value).decimalPlaces(6).toFixed(6);
   }
 
+  function isEmptyDecimal(value) {
+    return value == null || String(value).trim() === '';
+  }
+
+  function decimalEquals(left, right) {
+    const leftEmpty = isEmptyDecimal(left);
+    const rightEmpty = isEmptyDecimal(right);
+    if (leftEmpty || rightEmpty) return leftEmpty && rightEmpty;
+    const leftDecimal = new BigNumber(left);
+    const rightDecimal = new BigNumber(right);
+    if (!leftDecimal.isFinite() || !rightDecimal.isFinite()) {
+      return String(left) === String(right);
+    }
+    return leftDecimal.eq(rightDecimal);
+  }
+
+  const DECIMAL_INPUT_PATTERN = /^\d{1,12}(?:\.\d{1,6})?$/;
+
   export default {
     data() {
       return {
@@ -485,9 +497,9 @@
       createTarget(targetDetailId) {
         return {
           targetDetailId,
-          measureQuantity: 0,
-          packageQuantity: 0,
-          weight: 0
+          measureQuantity: '0',
+          packageQuantity: '0',
+          weight: '0'
         };
       },
       reset() {
@@ -527,28 +539,66 @@
         this.lockRows.forEach((row) => {
           row.targetDetailId = 1;
         });
+        const target = this.targets[0];
+        target.measureQuantity = this.sum(this.sources, 'stockCountBase');
+        target.packageQuantity = this.sum(this.sources, 'packingQuantity');
+        target.weight = this.sum(this.sources, 'weight');
+        this.syncTargetPackingMinimums();
+      },
+      /** 包装分配变化时,同步其关联锁定单的全部明细和包装。 */
+      assignPackingTarget(packing, targetDetailId) {
+        const relatedLocks = this.lockRows.filter((lock) =>
+          lock.lockedPackingStockIds.some(
+            (id) => String(id) === String(packing.packingStockId)
+          )
+        );
+        if (relatedLocks.length) {
+          this.assignConnectedLockRows(relatedLocks, targetDetailId);
+        }
         this.syncTargetPackingMinimums();
       },
-      /** 包装目标变化后,关联包装的锁定明细必须自动落到同一目标。 */
-      syncLockTargets() {
-        const targetByPackingId = this.packingRows.reduce((map, row) => {
-          map[String(row.packingStockId)] = row.targetDetailId;
-          return map;
-        }, {});
+      /** 锁定单必须整单分配,且所有关联包装必须与锁定单落到同一目标。 */
+      assignLockOrderTarget(lock, targetDetailId) {
+        this.assignConnectedLockRows([lock], targetDetailId);
+        this.syncTargetPackingMinimums();
+      },
+      /** 传播同锁定单及共享包装关系,确保整个关联组只有一个目标。 */
+      assignConnectedLockRows(seedLocks, targetDetailId) {
+        const lockOrderIds = new Set(
+          seedLocks.map((lock) => String(lock.lockOrderId))
+        );
+        const packingIds = new Set();
+        let changed = true;
+        while (changed) {
+          changed = false;
+          this.lockRows.forEach((lock) => {
+            const linkedToOrder = lockOrderIds.has(String(lock.lockOrderId));
+            const linkedToPacking = lock.lockedPackingStockIds.some((id) =>
+              packingIds.has(String(id))
+            );
+            if (!linkedToOrder && !linkedToPacking) return;
+            if (!lockOrderIds.has(String(lock.lockOrderId))) {
+              lockOrderIds.add(String(lock.lockOrderId));
+              changed = true;
+            }
+            lock.lockedPackingStockIds.forEach((id) => {
+              if (!packingIds.has(String(id))) {
+                packingIds.add(String(id));
+                changed = true;
+              }
+            });
+          });
+        }
         this.lockRows.forEach((lock) => {
-          if (!lock.lockedPackingStockIds.length) return;
-          const targetIds = lock.lockedPackingStockIds
-            .map((id) => targetByPackingId[String(id)])
-            .filter(Boolean);
-          const distinct = Array.from(new Set(targetIds));
-          lock.targetConflict = distinct.length > 1;
-          lock.targetDetailId =
-            !lock.targetConflict &&
-            targetIds.length === lock.lockedPackingStockIds.length
-              ? distinct[0]
-              : null;
+          if (!lockOrderIds.has(String(lock.lockOrderId))) return;
+          lock.targetDetailId = targetDetailId;
+          lock.targetConflict = false;
+        });
+        this.packingRows.forEach((packing) => {
+          if (packingIds.has(String(packing.packingStockId))) {
+            packing.targetDetailId = targetDetailId;
+          }
         });
-        this.syncTargetPackingMinimums();
       },
       /** 包装变更后同步目标下限,超出包装汇总的部分作为散量。 */
       syncTargetPackingMinimums() {
@@ -559,10 +609,10 @@
           const measureMinimum = decimal(this.sum(packages, 'measureQuantity'));
           const weightMinimum = decimal(this.sum(packages, 'weight'));
           if (decimal(target.measureQuantity).lt(measureMinimum)) {
-            target.measureQuantity = Number(measureMinimum.toFixed(6));
+            target.measureQuantity = measureMinimum.toFixed(6);
           }
           if (decimal(target.weight).lt(weightMinimum)) {
-            target.weight = Number(weightMinimum.toFixed(6));
+            target.weight = weightMinimum.toFixed(6);
           }
         });
       },
@@ -648,23 +698,29 @@
       },
       getBatchAttributeDifferences() {
         const attributes = [
+          { key: 'stockId', label: '库存台账ID' },
           { key: 'warehouseId', label: '仓库', displayKey: 'warehouseName' },
           { key: 'batchNo', label: '业务批次号' },
+          { key: 'categoryId', label: '物品ID' },
           { key: 'categoryCode', label: '物品编码' },
           { key: 'categoryName', label: '物品名称' },
           { key: 'specification', label: '规格' },
           { key: 'categoryModel', label: '型号' },
           { key: 'brandNum', label: '牌号' },
           { key: 'colorKey', label: '颜色' },
-          { key: 'modelKey', label: '机型' },
-          { key: 'unitPrice', label: '单价' },
-          { key: 'supplierId', label: '供应商', displayKey: 'supplierName' }
+          { key: 'unitPrice', label: '单价', decimal: true },
+          { key: 'supplierId', label: '供应商ID' },
+          { key: 'supplierName', label: '供应商' },
+          { key: 'supplierCode', label: '供应商代号' }
         ];
         return attributes.reduce((differences, attribute) => {
-          const values = this.sources.map((row) =>
-            String(row[attribute.key] == null ? '' : row[attribute.key])
-          );
-          if (new Set(values).size < 2) return differences;
+          const values = this.sources.map((row) => row[attribute.key]);
+          const firstValue = values[0];
+          const hasDifference = attribute.decimal
+            ? values.some((value) => !decimalEquals(firstValue, value))
+            : new Set(values.map((value) => String(value == null ? '' : value)))
+                .size > 1;
+          if (!hasDifference) return differences;
           const displayValues = Array.from(
             new Set(
               this.sources.map((row) => {
@@ -686,12 +742,43 @@
       validateAllocation() {
         if (this.mode === 'split' && this.targets.length < 2)
           return '拆批至少需要两个目标子批次';
+        if (
+          this.mode === 'split' &&
+          this.targets.some((target) =>
+            ['measureQuantity', 'packageQuantity', 'weight'].some(
+              (key) => !DECIMAL_INPUT_PATTERN.test(String(target[key]).trim())
+            )
+          )
+        )
+          return '计量数量、包装数量和重量最多支持12位整数、6位小数';
         if (this.packingRows.some((row) => !row.targetDetailId))
           return '请为每条包装明细选择目标子批次';
         if (
           this.lockRows.some((row) => row.targetConflict || !row.targetDetailId)
         )
           return '请完成锁定单分配,关联包装必须属于同一目标';
+        const lockOrderTargets = new Map();
+        for (const lock of this.lockRows) {
+          const lockOrderId = String(lock.lockOrderId);
+          const assignedTarget = lockOrderTargets.get(lockOrderId);
+          if (
+            lockOrderTargets.has(lockOrderId) &&
+            assignedTarget !== lock.targetDetailId
+          ) {
+            return '同一锁定单的全部明细必须分配到同一目标子批次';
+          }
+          lockOrderTargets.set(lockOrderId, lock.targetDetailId);
+          const packingTargetMismatch = lock.lockedPackingStockIds.some(
+            (id) => {
+              const packing = this.packingRows.find(
+                (row) => String(row.packingStockId) === String(id)
+              );
+              return !packing || packing.targetDetailId !== lock.targetDetailId;
+            }
+          );
+          if (packingTargetMismatch)
+            return '锁定单关联包装必须分配到同一目标子批次';
+        }
         if (this.targetRows.some((row) => decimal(row.measureQuantity).lte(0)))
           return '各目标子批次计量数量必须大于零';
         if (