2026-09-09 10:47:24 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-10 10:14:17 +08:00
|
|
|
|
def _stock_models():
|
|
|
|
|
|
"""
|
|
|
|
|
|
延迟导入三张实物库存表。
|
|
|
|
|
|
|
|
|
|
|
|
★ material_base 表本身没有 sku 列,SKU 只存在于 stock_buy / stock_semi /
|
|
|
|
|
|
stock_product,所以按 SKU 反查物料时必须先经库存表做 sku → base_id 映射。
|
2026-09-09 10:47:24 +08:00
|
|
|
|
"""
|
2026-09-10 10:14:17 +08:00
|
|
|
|
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]
|
2026-09-09 10:47:24 +08:00
|
|
|
|
|
2026-09-10 10:14:17 +08:00
|
|
|
|
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 核心规格一致(斜杠前段,或前端未传规格视为防呆拦截)。
|
2026-09-09 10:47:24 +08:00
|
|
|
|
"""
|
|
|
|
|
|
flagged = []
|
|
|
|
|
|
seen = set()
|
2026-09-10 10:14:17 +08:00
|
|
|
|
sku_cache = {}
|
2026-09-09 10:47:24 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-10 10:14:17 +08:00
|
|
|
|
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. 查出所有“启用且需审批”的危险物料库(字符串兜底时用)
|
2026-09-09 10:47:24 +08:00
|
|
|
|
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')
|
2026-09-10 10:14:17 +08:00
|
|
|
|
sku = item.get('sku')
|
|
|
|
|
|
source_table = item.get('source_table')
|
2026-09-09 10:47:24 +08:00
|
|
|
|
|
2026-09-10 10:14:17 +08:00
|
|
|
|
# --- 优先级 1:base_id 绝对判定 ---
|
2026-09-09 10:47:24 +08:00
|
|
|
|
if base_id is not None:
|
|
|
|
|
|
bid = int(base_id) if _is_int(base_id) else 0
|
|
|
|
|
|
if bid > 0:
|
2026-09-10 10:14:17 +08:00
|
|
|
|
_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)
|
|
|
|
|
|
# ★ 查到物料即定论,不再走字符串兜底
|
2026-09-09 10:47:24 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
2026-09-10 10:14:17 +08:00
|
|
|
|
# --- 优先级 3:无 ID 且 SKU 查无结果时的降级兜底 (名称精确相等 AND 核心规格相等) ---
|
2026-09-09 10:47:24 +08:00
|
|
|
|
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
|