From 8b61cdb67de2d8f5ee61a798d2b9dab084a95b54 Mon Sep 17 00:00:00 2001 From: yueli Date: Thu, 10 Sep 2026 10:14:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(approval):=20=E9=9C=80=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E6=94=B9=E4=B8=BA=20base=5Fid=20=E2=86=92=20?= =?UTF-8?q?SKU=20=E2=86=92=20=E5=90=8D=E7=A7=B0+=E8=A7=84=E6=A0=BC=20?= =?UTF-8?q?=E4=B8=89=E7=BA=A7=E9=99=8D=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原判定仅有 base_id 与「名称+规格字符串」两级,出库申请明细只有 name/spec、 无 base_id,只能走脆弱的字符串匹配,同名或同规格不同料极易误判。 新增 SKU 优先级:material_base 表本身没有 sku 列,SKU 只存在于 stock_buy / stock_semi / stock_product,故经明细自带 source_table 反查 sku → base_id 再回查物料,查不到才依次降级其余两张库存表。 查到物料即定论,不再往下走字符串兜底,杜绝误杀。 --- .../app/services/approval_control.py | 91 ++++++++++++++++--- 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/inventory-backend/app/services/approval_control.py b/inventory-backend/app/services/approval_control.py index 5e8327b..7ca15a7 100644 --- a/inventory-backend/app/services/approval_control.py +++ b/inventory-backend/app/services/approval_control.py @@ -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