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