feat(stock): status 硬隔离与出库通道封堵
激活库存表长期「只写不读」的 status 列,使其成为分配准入的硬门槛。 - inventory_reservation: 新增状态语义单一事实来源(STOCK_STATUS_*/ allocatable_filter/is_allocatable);verify_scanned 增加状态准入校验 (覆盖出库与借库两条提交路径) - outbound: _allocate_bom_requirements 过滤条件加 allocatable_filter, 非「在库」一律不进入候选集;扫码路由把 ValueError 转为 400 - outbound_service: create_outbound_batch 强制 request_id 必填。原先无单 时会跳到所谓「散单」分支,而该分支的扣减逻辑从未落地(注释点名的 _apply_reservation_override 全仓不存在),实测只写台账不扣库存, 同一批货可被反复出库;get_stock_by_barcode 接入扫码准入;维修单扫码 补排除「报废转出」;预占再平衡块补 rollback 兜底 - fill_missing_stock_status.sql: 把存量行刷回「在库」。该过滤是 Fail-Closed 的,不先洗刷会让全库无法出库 破坏性变更:不传 request_id 的出库请求现在会被拒绝。
This commit is contained in:
@ -148,6 +148,11 @@ def scan_barcode():
|
||||
'msg': '未找到对应的库存记录,请确认条码是否正确'
|
||||
}), 404
|
||||
|
||||
except ValueError as e:
|
||||
# ★ 业务性拒绝(物料状态异常等):属于「扫到了但按规定不能出」,
|
||||
# 不是系统故障 —— 返回 400 + 明确文案,便于前端红字提示工人。
|
||||
# 若落到下方 500 分支,前端只会显示「服务器错误」,工人无从判断。
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'扫描查询出错: {str(e)}'}), 500
|
||||
@ -318,6 +323,16 @@ def _allocate_bom_requirements(requirements, company_limit,
|
||||
# 按 (base_id, source_table) 归集
|
||||
rows_by_base = {}
|
||||
|
||||
# ★ 状态门槛:本函数是**全系统库存分配的唯一权威入口** ——
|
||||
# 出库申请与借库申请都经 reserve_for_items() 走到这里拿候选行,
|
||||
# 故在此处加一条即对两者同时生效(报废不走分配器,见 scrap_approval_service)。
|
||||
# 规则见 inventory_reservation.allocatable_filter:仅「在库」可被分配,
|
||||
# 「冻结」「不良品」以及 status 为 NULL 的行一律不进入候选集。
|
||||
#
|
||||
# Fail-Closed 说明:若该条件导致查询异常,下方 except 会 continue 掉整张表,
|
||||
# 表现为「该物料无库存」而非「放行坏件」,方向上是安全的。
|
||||
from app.services.inventory_reservation import allocatable_filter
|
||||
|
||||
for model, source_table, type_label, type_key in (
|
||||
(StockBuy, 'stock_buy', '采购件', 'material'),
|
||||
(StockSemi, 'stock_semi', '半成品', 'semi'),
|
||||
@ -327,6 +342,7 @@ def _allocate_bom_requirements(requirements, company_limit,
|
||||
q = model.query.filter(
|
||||
model.base_id.in_(base_ids),
|
||||
model.available_quantity > 0, # ★ 只取真正可用的行
|
||||
allocatable_filter(model), # ★ 硬隔离:非「在库」一律不出货
|
||||
)
|
||||
if company_limit is not None:
|
||||
q = q.filter(model.base.has(MaterialBase.company_name == company_limit))
|
||||
|
||||
Reference in New Issue
Block a user