fix(approval): 需审批判定改为 base_id → SKU → 名称+规格 三级降级

原判定仅有 base_id 与「名称+规格字符串」两级,出库申请明细只有 name/spec、
无 base_id,只能走脆弱的字符串匹配,同名或同规格不同料极易误判。

新增 SKU 优先级:material_base 表本身没有 sku 列,SKU 只存在于
stock_buy / stock_semi / stock_product,故经明细自带 source_table 反查
sku → base_id 再回查物料,查不到才依次降级其余两张库存表。
查到物料即定论,不再往下走字符串兜底,杜绝误杀。
This commit is contained in:
yueli
2026-09-10 10:14:17 +08:00
parent 98e657e289
commit 8b61cdb67d

View File

@ -26,18 +26,70 @@ def _is_int(v):
return False
def _stock_models():
"""
延迟导入三张实物库存表。
★ material_base 表本身没有 sku 列,SKU 只存在于 stock_buy / stock_semi /
stock_product,所以按 SKU 反查物料时必须先经库存表做 sku → base_id 映射。
"""
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
return {
'stock_buy': StockBuy,
'stock_semi': StockSemi,
'stock_product': StockProduct,
}
def _material_by_sku(sku, source_table=None, cache=None):
"""
按 SKU 反查物料主表(经库存表 sku → base_id 映射后回查 material_base)。
优先用明细自带的 source_table 定位,命中不了再依次降级查其余两张表。
返回 MaterialBase 或 None(None 表示该 SKU 在库存表中查无对应物料)。
"""
sku = str(sku or '').strip()
if not sku:
return None
cache = cache if cache is not None else {}
if sku in cache:
return cache[sku]
models = _stock_models()
order = ([source_table] if source_table in models else []) + [
k for k in models if k != source_table
]
found = None
for key in order:
row = models[key].query.filter_by(sku=sku).first()
if row and row.base_id:
m = MaterialBase.query.get(int(row.base_id))
if m:
found = m
break
cache[sku] = found
return found
def resolve_approval_control(items):
"""
判断一批出库/借库申请明细是否需要审批。
判断一批出库/借库/报废申请明细是否需要审批。
判定优先级:
1) 方案1(最高):明细带有效 base_id → 直接按物料主键查 is_approval_required,
且只要有有效 ID,无论命中与否都不再做任何字符串兜底(杜绝同名/同规格误杀)。
2) 方案2(无 ID 兜底):名称必须 100% 精确一致 AND 核心规格一致(斜杠前段,
或前端未传规格视为防呆拦截),两条件同时成立才判定命中。
判定优先级(逐级降级,任一级给出结论即停止,不再往下兜底):
1) base_id —— 物料主键,最权威。只要有有效 ID,无论是否命中需审批都不再做字符串匹配
(杜绝同名/同规格误杀)。
2) SKU —— 经库存表 sku→base_id 反查物料。查到即定论;查无结果才降级。
3) 名称 + 规格 —— 仅当 base_id 与 SKU 均缺失、或两者均查无结果时才启用的字符串兜底。
名称必须 100% 精确一致 AND 核心规格一致(斜杠前段,或前端未传规格视为防呆拦截)。
"""
flagged = []
seen = set()
sku_cache = {}
def _record(m_obj, req_name):
if m_obj.id in seen:
@ -49,25 +101,38 @@ def resolve_approval_control(items):
'base_id': m_obj.id,
})
# 1. 查出所有“启用且需审批”的危险物料库
def _decide_by_material(m_obj, req_name):
"""给出物料的权威结论:命中需审批则记录;无论命中与否都不再降级"""
if m_obj and m_obj.is_enabled and m_obj.is_approval_required:
_record(m_obj, req_name)
# 1. 查出所有“启用且需审批”的危险物料库(字符串兜底时用)
flagged_materials = _materials_flagged()
for item in items or []:
name = str(item.get('name') or '').strip()
spec = str(item.get('spec_model') or '').strip()
base_id = item.get('base_id')
sku = item.get('sku')
source_table = item.get('source_table')
# --- 方案 1:最高优先级,依据 base_id 绝对判定 ---
# --- 优先级 1:base_id 绝对判定 ---
if base_id is not None:
bid = int(base_id) if _is_int(base_id) else 0
if bid > 0:
m = MaterialBase.query.get(bid)
if m and m.is_enabled and m.is_approval_required:
_record(m, name)
# ★ 关键:只要有有效 ID,无论是否需审批,绝不允许往下走字符串兜底
_decide_by_material(MaterialBase.query.get(bid), name)
# ★ 关键:只要有有效 ID,无论是否需审批,绝不允许往下走 SKU/字符串兜底
continue
# --- 方案 2:无 ID 时的降级兜底 (名称精确相等 AND 核心规格相等) ---
# --- 优先级 2:SKU 反查物料 ---
if str(sku or '').strip():
m = _material_by_sku(sku, source_table, sku_cache)
if m:
_decide_by_material(m, name)
# ★ 查到物料即定论,不再走字符串兜底
continue
# --- 优先级 3:无 ID 且 SKU 查无结果时的降级兜底 (名称精确相等 AND 核心规格相等) ---
if not name:
continue