From d06ce45a7258217aaefff2bd3a484738d3fd27ec Mon Sep 17 00:00:00 2001 From: yueli Date: Wed, 9 Sep 2026 10:47:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(borrow,outbound):=20=E9=9C=80=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E5=88=A4=E5=AE=9A=E6=94=B6=E6=95=9B=E2=80=94=E2=80=94?= =?UTF-8?q?ID=E4=BC=98=E5=85=88=20+=20=E5=90=8D=E7=A7=B0=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?AND=E6=A0=B8=E5=BF=83=E8=A7=84=E6=A0=BC(=E6=96=9C=E6=9D=A0?= =?UTF-8?q?=E5=89=8D=E6=AE=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - approval_control 重写:带 base_id 按主键绝对判定;无 id 才名称100%一致 AND 核心规格(_code_of)一致兜底,杜绝同名不同规误杀 - 借/出库服务命中需审批但未选审批人时,报错列出需审批物料名/规格 --- .../app/services/approval_control.py | 88 +++++++++++++++++++ .../app/services/borrow_service.py | 6 +- .../app/services/outbound_service.py | 6 +- 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 inventory-backend/app/services/approval_control.py diff --git a/inventory-backend/app/services/approval_control.py b/inventory-backend/app/services/approval_control.py new file mode 100644 index 0000000..5e8327b --- /dev/null +++ b/inventory-backend/app/services/approval_control.py @@ -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 diff --git a/inventory-backend/app/services/borrow_service.py b/inventory-backend/app/services/borrow_service.py index 2e5c436..9f77335 100644 --- a/inventory-backend/app/services/borrow_service.py +++ b/inventory-backend/app/services/borrow_service.py @@ -104,9 +104,11 @@ class BorrowApprovalService: 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: - raise ValueError("该申请包含需审批的物料,请选择审批人后再提交") + _names = ";".join(f"{m['name']}({m['spec_model'] or '-'})" for m in flagged_materials) + raise ValueError(f"以下物料需审批出库/借库:{_names}。请选择审批人后再提交") if approver_id: allowed_approvers = [{"type": "user", "value": int(approver_id)}] diff --git a/inventory-backend/app/services/outbound_service.py b/inventory-backend/app/services/outbound_service.py index ec68816..1818116 100644 --- a/inventory-backend/app/services/outbound_service.py +++ b/inventory-backend/app/services/outbound_service.py @@ -773,9 +773,11 @@ class OutboundApprovalService: 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: - raise ValueError("该申请包含需审批的物料,请选择审批人后再提交") + _names = ";".join(f"{m['name']}({m['spec_model'] or '-'})" for m in flagged_materials) + raise ValueError(f"以下物料需审批出库/借库:{_names}。请选择审批人后再提交") # ★ 指定审批人模式:approver_id 覆盖 allowed_approvers if approver_id: