fix(outbound): 库存分配按 base_id 合并需求,修复重复行导致的假性库存不足
_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」(走正常缺料分支)
This commit is contained in:
@ -448,7 +448,22 @@ def _allocate_bom_requirements(requirements, company_limit,
|
|||||||
from sqlalchemy.orm import joinedload # ★ 必须在此导入:本函数模块级作用域不可见
|
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 = []
|
reqs = []
|
||||||
|
merged = {}
|
||||||
for r in requirements:
|
for r in requirements:
|
||||||
try:
|
try:
|
||||||
bid = int(r.get('base_id'))
|
bid = int(r.get('base_id'))
|
||||||
@ -460,8 +475,13 @@ def _allocate_bom_requirements(requirements, company_limit,
|
|||||||
need = 0.0
|
need = 0.0
|
||||||
if bid <= 0 or need <= 0:
|
if bid <= 0 or need <= 0:
|
||||||
continue
|
continue
|
||||||
reqs.append({'base_id': bid, 'required_qty': need,
|
hit = merged.get(bid)
|
||||||
'name': r.get('name') or '', 'spec_model': r.get('spec_model') or ''})
|
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:
|
if not reqs:
|
||||||
return jsonify({'code': 400, 'msg': 'requirements 中无有效的 base_id/required_qty'}), 400
|
return jsonify({'code': 400, 'msg': 'requirements 中无有效的 base_id/required_qty'}), 400
|
||||||
|
|||||||
Reference in New Issue
Block a user