feat(borrow,outbound): 需审批判定收敛——ID优先 + 名称精确AND核心规格(斜杠前段)

- approval_control 重写:带 base_id 按主键绝对判定;无 id 才名称100%一致 AND 核心规格(_code_of)一致兜底,杜绝同名不同规误杀
- 借/出库服务命中需审批但未选审批人时,报错列出需审批物料名/规格
This commit is contained in:
yueli
2026-09-09 10:47:24 +08:00
parent 2abac8b3f9
commit d06ce45a72
3 changed files with 96 additions and 4 deletions

View File

@ -0,0 +1,88 @@
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 resolve_approval_control(items):
"""
判断一批出库/借库申请明细是否需要审批。
判定优先级:
1) 方案1最高明细带有效 base_id → 直接按物料主键查 is_approval_required
且只要有有效 ID无论命中与否都不再做任何字符串兜底杜绝同名/同规格误杀)。
2) 方案2无 ID 兜底):名称必须 100% 精确一致 AND 核心规格一致(斜杠前段,
或前端未传规格视为防呆拦截),两条件同时成立才判定命中。
"""
flagged = []
seen = set()
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,
})
# 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')
# --- 方案 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无论是否需审批绝不允许往下走字符串兜底
continue
# --- 方案 2无 ID 时的降级兜底 (名称精确相等 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

View File

@ -104,9 +104,11 @@ class BorrowApprovalService:
raise ValueError(f"{idx + 1} 条物品的 quantity 格式无效: {str(e)}") raise ValueError(f"{idx + 1} 条物品的 quantity 格式无效: {str(e)}")
# ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过 # ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过
need_approval = BorrowApprovalService._items_require_approval(items) from app.services.approval_control import resolve_approval_control
need_approval, flagged_materials = resolve_approval_control(items)
if need_approval and not approver_id: if need_approval and not approver_id:
raise ValueError("该申请包含需审批的物料,请选择审批人后再提交") _names = "".join(f"{m['name']}{m['spec_model'] or '-'}" for m in flagged_materials)
raise ValueError(f"以下物料需审批出库/借库:{_names}。请选择审批人后再提交")
if approver_id: if approver_id:
allowed_approvers = [{"type": "user", "value": int(approver_id)}] allowed_approvers = [{"type": "user", "value": int(approver_id)}]

View File

@ -773,9 +773,11 @@ class OutboundApprovalService:
raise ValueError(f"{idx + 1} 条物品的 quantity 格式无效: {str(e)}") raise ValueError(f"{idx + 1} 条物品的 quantity 格式无效: {str(e)}")
# ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过 # ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过
need_approval = OutboundApprovalService._items_require_approval(items) from app.services.approval_control import resolve_approval_control
need_approval, flagged_materials = resolve_approval_control(items)
if need_approval and not approver_id: if need_approval and not approver_id:
raise ValueError("该申请包含需审批的物料,请选择审批人后再提交") _names = "".join(f"{m['name']}{m['spec_model'] or '-'}" for m in flagged_materials)
raise ValueError(f"以下物料需审批出库/借库:{_names}。请选择审批人后再提交")
# ★ 指定审批人模式approver_id 覆盖 allowed_approvers # ★ 指定审批人模式approver_id 覆盖 allowed_approvers
if approver_id: if approver_id: