fix(reservation): 实扫来源不可识别时整单失败,避免预占永久锁死

出库与借库两条执行路径原先写作:

    _scanned = [i for i in items if i.get('source_table') != 'trans_repair']
    if _scanned:
        verify_scanned(...); restore_then_deduct(...)

`if _scanned:` 为假时校验与释放被整体跳过,而下方仍无条件把单据置为
status=3(已完成)。此时申请阶段 reserve_for_items() 扣掉的 available_quantity
无人归还,且 status=3 之后 /close(要求 status==1)与 /withdraw(要求
status∈(0,1))都拒绝再释放 —— 预占就此永久锁死,成为谁也领不走的幽灵库存,
且没有流水可供追溯。

改为 Fail-Closed:实扫明细的来源必须全部落在 model_map 内,否则整单失败。
事务回滚后单据保持 status=1、预占原样保留,库管可重试执行,或走驳回/撤回 ——
任一路径都能正常归还预占。

- outbound_service.create_outbound_batch:以 model_map 白名单过滤,
  并要求 _scanned 非空
- trans_service.execute_dispatch:先显式校验来源合法性并列出非法值,
  再要求 _scanned 非空
This commit is contained in:
yueli
2026-09-11 10:34:55 +08:00
parent b313fefdfa
commit 06cb5f1cac
2 changed files with 43 additions and 9 deletions

View File

@ -258,11 +258,22 @@ class OutboundService:
verify_scanned, restore_then_deduct,
)
_approved = approval.get_items()
# 仅处理库存类来源;维修单(trans_repair)不走库存预占
_scanned = [i for i in items if i.get('source_table') != 'trans_repair']
if _scanned:
verify_scanned(_scanned, _approved)
restore_then_deduct(_scanned, _approved)
# ★ Fail-Closed:只认库存类来源。只要一条库存物料都没扫到就整单失败。
#
# 旧代码写作 `_scanned = [... if source_table != 'trans_repair']`
# 再 `if _scanned:` —— 一旦为假,校验与释放被整体跳过,而下方仍
# 无条件执行 approval.status = 3。此时申请阶段 reserve_for_items()
# 扣掉的 available_quantity 无人归还,且 status=3 没有任何释放入口
# (/close 要求 status==1、/withdraw 要求 status∈(0,1)),预占即
# 永久锁死 —— 幽灵库存。
# 现在直接要求 _scanned 非空:维修单等非库存来源一并被挡在门外。
_scanned = [i for i in items if i.get('source_table') in model_map]
if not _scanned:
raise ValueError("未扫描到有效的库存物料,出库失败,请检查扫码内容")
verify_scanned(_scanned, _approved)
restore_then_deduct(_scanned, _approved)
try:
for item in items: