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:
@ -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:
|
||||
|
||||
@ -101,14 +101,37 @@ class TransService:
|
||||
# · 借库转报废(scrap_borrow)在确认损失时扣 stock,其注释明确假设
|
||||
# 「可用库存已在借出时冻结」—— 若借出已扣会重复扣减。
|
||||
# ==============================================================
|
||||
# ★ 防线 2.5(Fail-Closed):实扫明细的 source_table 必须全部可识别。
|
||||
#
|
||||
# 原先此处写作 `if _scanned_for_check:` —— 不在 model_map 的明细会被
|
||||
# 静默过滤。若整批明细都不可识别,校验与释放被整体跳过,下方主循环
|
||||
# 也逐条 `continue`,末尾却照常把 approval.status 置为 3,于是:
|
||||
# · 申请阶段预占的 available_quantity 无人释放 → 幽灵库存永久锁死;
|
||||
# · 单据已非 status=1,/close 与 /withdraw 都拒绝再释放;
|
||||
# · 且没有任何 TransBorrow 流水可供追溯。
|
||||
# 现在改为整单失败:事务回滚,单据保持 status=1、预占原样保留,
|
||||
# 库管可重试执行,或走驳回/撤回 —— 任一路径都能正常归还预占。
|
||||
# ==============================================================
|
||||
unknown_source = sorted({
|
||||
str(i.get('source_table')) for i in items
|
||||
if not isinstance(i.get('source_table'), str)
|
||||
or i.get('source_table') not in model_map
|
||||
})
|
||||
if unknown_source:
|
||||
raise ValueError(
|
||||
f"实扫明细的库存来源不合法:{'、'.join(unknown_source)},"
|
||||
f"仅支持 {'、'.join(sorted(model_map))}"
|
||||
)
|
||||
|
||||
# 经上方校验后 source_table 必然合法,故本列表与 items 一一对应,
|
||||
# 不存在「被过滤掉的明细」。空列表由 verify_scanned 兜底报错。
|
||||
_scanned_for_check = [
|
||||
{'source_table': i.get('source_table'), 'stock_id': i.get('id'),
|
||||
'quantity': i.get('out_quantity')}
|
||||
for i in items if i.get('source_table') in model_map
|
||||
for i in items
|
||||
]
|
||||
if _scanned_for_check:
|
||||
verify_scanned(_scanned_for_check, approved_items)
|
||||
restore_then_deduct(_scanned_for_check, approved_items, deduct_stock=False)
|
||||
verify_scanned(_scanned_for_check, approved_items)
|
||||
restore_then_deduct(_scanned_for_check, approved_items, deduct_stock=False)
|
||||
|
||||
# 累计本次扫码出库量(用于下方防线4的二次校验)
|
||||
dispatch_acc = {}
|
||||
|
||||
Reference in New Issue
Block a user