Files
KCGL/inventory-backend/app/services/approval_control.py
yueli 8b61cdb67d 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 再回查物料,查不到才依次降级其余两张库存表。
查到物料即定论,不再往下走字符串兜底,杜绝误杀。
2026-09-10 10:14:17 +08:00

154 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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