from app.models.base import MaterialBase def _code_of(spec): """取规格的首段编码('S0013/S0013' -> 'S0013';'S0013' -> 'S0013')""" s = (spec or '').strip() if not s: return '' return s.split('/')[0].strip().upper() def _materials_flagged(): """一次性取出所有“启用且需审批”的物料(量小,逐项判定前先全量拉一次)""" rows = MaterialBase.query.filter( MaterialBase.is_enabled == True, MaterialBase.is_approval_required == True ).all() return rows def _is_int(v): try: int(v) return True except (TypeError, ValueError): 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) 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: return seen.add(m_obj.id) flagged.append({ 'name': m_obj.name or req_name, 'spec_model': m_obj.spec_model or '', 'base_id': m_obj.id, }) 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 绝对判定 --- if base_id is not None: bid = int(base_id) if _is_int(base_id) else 0 if bid > 0: _decide_by_material(MaterialBase.query.get(bid), name) # ★ 关键:只要有有效 ID,无论是否需审批,绝不允许往下走 SKU/字符串兜底 continue # --- 优先级 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 # 提取当前申请行的核心规格 req_core_spec = _code_of(spec) for fm in flagged_materials: # 条件 A:名称必须 100% 绝对一致 if fm.name != name: continue # 条件 B:核心规格一致 (或前端未传规格视为防呆拦截) fm_core_spec = _code_of(fm.spec_model) if not req_core_spec or req_core_spec == fm_core_spec: _record(fm, name) break # 命中即判定本申请行需审批,跳出内层循环 return bool(flagged), flagged