|
|
@@ -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 (
|