From fe54d013916eaafb74eda7de4a82de6c13732145 Mon Sep 17 00:00:00 2001 From: yueli Date: Tue, 15 Sep 2026 13:21:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(outbound):=20=E5=BA=8F=E5=88=97=E5=8F=B7?= =?UTF-8?q?=E7=89=A9=E6=96=99=E5=87=BA=E5=BA=93=E8=A2=AB=E8=AF=AF=E5=88=A4?= =?UTF-8?q?=E8=B6=85=E5=87=BA=E8=AE=A1=E5=88=92=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:申请单里同一个物料有 2 个,扫第 2 个时报 「超出计划数量(计划: 1,已扫: 1,本次: 1)」。 根因:计划侧与已扫侧用了**不同的聚合方式**。 已扫侧 .filter().reduce() → 把【所有】匹配行加总 计划侧 .find() → 只取【第一条】匹配行 序列号物料在申请单里是「一物一行、每行 quantity=1」(因为每个序列号对应 一条独立库存行),于是 planQty 只拿到 1,而 alreadyScanned 是全部之和, 1 + 1 > 1 直接判超发。普通数量制物料计划单只有一行,两边一致,所以一直 没暴露 —— 只有序列号物料会踩中。 同一缺陷还有第二处:unscannedList 逐行比较 planQty 与「累计已扫」, 导致两行各自都被同一笔扫码"满足",扫 1 个就把整个物料判为扫完、未扫清单漏报。 修复: - validateAgainstPlan:计划侧改为 .filter().reduce() 汇总所有匹配行 - unscannedList:先按「名称+规格」合并计划行,再与同口径汇总的已扫数比较 用申请单 410(Opt1025 两行各 1 个)的真实数据模拟验证: 修复前 扫第2个 → 超出计划数量(计划:1,已扫:1,本次:1) ← 与现场报错逐字一致 修复后 扫第2个 → 通过 修复后 扫第3个 → 仍被拦(计划:2,已扫:2,本次:1),超发保护未失效 --- inventory-web/src/views/outbound/create.vue | 52 ++++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/inventory-web/src/views/outbound/create.vue b/inventory-web/src/views/outbound/create.vue index 4f91821..ba3d69e 100644 --- a/inventory-web/src/views/outbound/create.vue +++ b/inventory-web/src/views/outbound/create.vue @@ -498,23 +498,35 @@ const scanTotalTypes = computed(() => plannedItems.value.length) // ★ 未扫清单:计划中还没扫满的物料 const unscannedList = computed(() => { - return plannedItems.value.map(plan => { - const planQty = Number(plan.quantity) || 0 - // 已扫数量(按名称+规格匹配) - const scannedQty = cartItems.value - .filter(ci => { - const ciName = (ci.name || '').trim() - const ciSpec = (ci.spec_model || ci.standard || '').trim() - return ciName === (plan.name || '').trim() && - ciSpec === (plan.spec_model || '').trim() - }) - .reduce((sum, ci) => sum + (Number(ci.out_quantity) || 0), 0) + // ★ 先按「名称+规格」把计划行**合并**再比较。 + // 序列号物料是一物一行(每行 quantity=1),若逐行比较,同一笔扫码会让 + // 每一行都算作"已满足"(planQty 1 - scannedQty 1 = 0),于是扫了 1 个 + // 就把整个物料判为扫完,未扫清单漏报。 + const planGroups = new Map() + for (const plan of plannedItems.value) { + const name = (plan.name || '').trim() + const spec = (plan.spec_model || '').trim() + const key = `${name}|${spec}` + const g = planGroups.get(key) || { name, spec, planQty: 0 } + g.planQty += Number(plan.quantity) || 0 + planGroups.set(key, g) + } + + // 已扫数量同样按「名称+规格」汇总 + const scannedByKey = new Map() + for (const ci of cartItems.value) { + const key = `${(ci.name || '').trim()}|${(ci.spec_model || ci.standard || '').trim()}` + scannedByKey.set(key, (scannedByKey.get(key) || 0) + (Number(ci.out_quantity) || 0)) + } + + return Array.from(planGroups.entries()).map(([key, g]) => { + const scannedQty = scannedByKey.get(key) || 0 return { - name: plan.name || '', - spec: plan.spec_model || '', - planQty, + name: g.name, + spec: g.spec, + planQty: g.planQty, scannedQty, - remaining: Math.max(0, planQty - scannedQty) + remaining: Math.max(0, g.planQty - scannedQty) } }).filter(item => item.remaining > 0) // 只保留未扫满的 }) @@ -751,17 +763,21 @@ const validateAgainstPlan = (scannedName: string, scannedSpec: string, scannedQt const normalizedName = scannedName.trim() const normalizedSpec = (scannedSpec || '').trim() - const matchedPlan = plannedItems.value.find(plan => { + // ★ 计划侧必须**汇总所有匹配行**,不能只取第一行。 + // 序列号物料在申请单里是「一物一行、每行 quantity=1」,用 .find() 只拿到 1, + // 而下面的已扫侧是 .reduce() 求总和 —— 两边聚合口径不一致, + // 扫到第 2 个就会误报「超出计划数量(计划: 1,已扫: 1,本次: 1)」。 + const matchedPlans = plannedItems.value.filter((plan: any) => { const planName = (plan.name || '').trim() const planSpec = (plan.spec_model || '').trim() return planName === normalizedName && planSpec === normalizedSpec }) - if (!matchedPlan) { + if (!matchedPlans.length) { return `该物料【${normalizedName} × ${normalizedSpec}】不在计划清单中,请检查` } - const planQty = matchedPlan.quantity ?? 0 + const planQty = matchedPlans.reduce((sum: number, plan: any) => sum + (Number(plan.quantity) || 0), 0) // 已扫数量(去重合并) const alreadyScanned = cartItems.value