From d7f7548cee253192525c2043b1d025ca2d04310c Mon Sep 17 00:00:00 2001 From: yueli Date: Sun, 20 Sep 2026 15:09:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(outbound):=20=E5=BA=93=E5=AD=98=E5=88=86?= =?UTF-8?q?=E9=85=8D=E6=8C=89=20base=5Fid=20=E5=90=88=E5=B9=B6=E9=9C=80?= =?UTF-8?q?=E6=B1=82=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=A4=8D=E8=A1=8C?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E5=81=87=E6=80=A7=E5=BA=93=E5=AD=98?= =?UTF-8?q?=E4=B8=8D=E8=B6=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _allocate_bom_requirements 在整轮 for req 循环中复用同一份可用量快照 (rows_by_base 建好后不再更新),而 for 循环本身不按 base_id 去重。同一 物料以多行进入时,每行都从同一份快照重新分配一遍,把同一个 stock_id 重复分配 N 次,累计分配量可超过该行真实可用量。 超配在分配阶段不会暴露,直到 reserve_for_items 的二次校验 (take > avail)才抛错,报「可用库存不足(需 X,实剩 Y)」,而实际库存 充足。实剩为 0.0 是当最大候选批次的可用量恰好等于单行需求时的收尾形态。 重复行来自正常业务,非脏数据: - 购物车里同一物料的多批次就是多行(Selection.vue 提交时只带 base_id + quantity,stock_id/source_table 被丢弃); - BOM 明细里同一子件被多处引用(前端 requirements 按 child_id 不去重); - 调拨 / 补发等拆行场景。 修复:在函数入口按 base_id 合并 reqs、required_qty 求和。分配语义不变 —— 分配器本就按 base_id 拉全量批次行、降序分配,输入里的 stock_id 从来 就被忽略。 选在此处收口而非合并调用方的 items:_allocate_bom_requirements 是全系统 库存分配的唯一权威入口(出库与借库都经 reserve_for_items 走到这里), 一处修改同时覆盖两条链路。 实测(base_id=711,四个批次可用量合计 124): 三行各 30 修复前 ERROR(需30/实剩10) → 修复后 OK,2117 出 70 + 1182 出 20 两行各 30 修复前静默超配(两行都绑 2117) → 修复后 OK,合并为 2117 出 60 十行各 30 修复后报真实缺口「需 300,可用 124」(走正常缺料分支) --- inventory-backend/app/api/v1/outbound.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/inventory-backend/app/api/v1/outbound.py b/inventory-backend/app/api/v1/outbound.py index 7b37368..d3e5019 100644 --- a/inventory-backend/app/api/v1/outbound.py +++ b/inventory-backend/app/api/v1/outbound.py @@ -448,7 +448,22 @@ def _allocate_bom_requirements(requirements, company_limit, from sqlalchemy.orm import joinedload # ★ 必须在此导入:本函数模块级作用域不可见 # 归一化需求,容忍字符串数字 + # + # ★ 必须按 base_id 合并。同一物料会以多行进入本函数,来源都是正常业务: + # · 购物车里同一物料的多批次就是多行(Selection.vue 提交时只带 + # base_id + quantity,stock_id 被丢弃); + # · BOM 明细里同一子件被多处引用(前端 requirements 按 child_id 不去重); + # · 调拨 / 补发等拆行场景。 + # 而下方分配是「按 base_id 拉全量批次行、降序分配」,且候选快照在整轮 + # for req 循环里**不更新** —— 同一个 base_id 出现 N 行,每行都会从同一份 + # 快照重新分配一遍,把同一个 stock_id 重复分配 N 次。 + # 超配在分配阶段不会暴露,直到 reserve_for_items 的二次校验 + # (take > avail)才炸,报「可用库存不足」而实际库存充足。 + # 合并后 required_qty 求和,分配语义不变(输入里的 stock_id 本就被忽略)。 + # ★ 不采用「合并入参 items」的方案:那是症状侧,_allocate_bom_requirements + # 才是全系统库存分配的唯一权威入口,在此收口可同时覆盖出库与借库。 reqs = [] + merged = {} for r in requirements: try: bid = int(r.get('base_id')) @@ -460,8 +475,13 @@ def _allocate_bom_requirements(requirements, company_limit, need = 0.0 if bid <= 0 or need <= 0: continue - reqs.append({'base_id': bid, 'required_qty': need, - 'name': r.get('name') or '', 'spec_model': r.get('spec_model') or ''}) + hit = merged.get(bid) + if hit is not None: + hit['required_qty'] += need + else: + merged[bid] = {'base_id': bid, 'required_qty': need, + 'name': r.get('name') or '', 'spec_model': r.get('spec_model') or ''} + reqs = list(merged.values()) if not reqs: return jsonify({'code': 400, 'msg': 'requirements 中无有效的 base_id/required_qty'}), 400