diff --git a/inventory-backend/app/api/v1/inbound/base.py b/inventory-backend/app/api/v1/inbound/base.py index 5ff6506..259eecc 100644 --- a/inventory-backend/app/api/v1/inbound/base.py +++ b/inventory-backend/app/api/v1/inbound/base.py @@ -539,6 +539,44 @@ def batch_set_inspection(): return jsonify({"code": 500, "msg": f"批量设置强制质检失败: {str(e)}"}), 500 +# ============================================================================== +# 2.8 批量设置“出库/借库需审批” (POST /api/v1/inbound/base/batch-approval) +# ============================================================================== +@inbound_base_bp.route('/batch-approval', methods=['POST']) +@permission_required('material_list:operation') +def batch_set_approval_required(): + """ + 批量设置物料“出库/借库需审批”标记(仿强制质检批量接口)。 + 请求体: { "ids": [1,2,3], "isApprovalRequired": true } + """ + try: + data = request.get_json() + if not data: + return jsonify({"code": 400, "msg": "No data provided"}), 400 + + ids = data.get('ids', []) + is_approval_required = bool(data.get('isApprovalRequired', False)) + if not ids: + return jsonify({"code": 400, "msg": "请选择要设置的物料"}), 400 + + updated_count = 0 + for base_id in ids: + material = MaterialBase.query.get(base_id) + if material: + material.is_approval_required = is_approval_required + updated_count += 1 + db.session.commit() + return jsonify({ + "code": 200, + "msg": f"批量设置成功,已更新 {updated_count} 条记录", + "data": {"updated": updated_count} + }) + except Exception as e: + db.session.rollback() + current_app.logger.error(f"批量设置需审批失败: {str(e)}") + return jsonify({"code": 500, "msg": f"批量设置需审批失败: {str(e)}"}), 500 + + # ============================================================================== # 2.7 智能分组求最大连号 API (GET /api/v1/inbound/base/spec-latest) # ============================================================================== diff --git a/inventory-backend/app/models/base.py b/inventory-backend/app/models/base.py index 809ce9c..9456015 100644 --- a/inventory-backend/app/models/base.py +++ b/inventory-backend/app/models/base.py @@ -39,6 +39,9 @@ class MaterialBase(db.Model): # 强制质检标记(采购入库时必须上传检测报告) is_inspection_required = db.Column(db.Boolean, default=False, comment='是否强制要求质检') + # 出库/借库需审批标记:命中该物料的出/借申请仍需走审批;否则默认自动通过、直接交库管执行 + is_approval_required = db.Column(db.Boolean, default=False, comment='出库/借库需审批') + # 参考价格 reference_price = db.Column(db.Numeric(10, 2), nullable=True, comment='参考价格') @@ -99,6 +102,8 @@ class MaterialBase(db.Model): 'isEnabled': bool(self.is_enabled), # 强制质检标记 'isInspectionRequired': bool(self.is_inspection_required), + # 出库/借库需审批标记 + 'isApprovalRequired': bool(self.is_approval_required), # 参考价格 'referencePrice': float(self.reference_price) if self.reference_price is not None else None, } diff --git a/inventory-backend/app/services/borrow_service.py b/inventory-backend/app/services/borrow_service.py index aedbfb4..2e5c436 100644 --- a/inventory-backend/app/services/borrow_service.py +++ b/inventory-backend/app/services/borrow_service.py @@ -33,6 +33,33 @@ class BorrowApprovalService: return f"APR-BOR-{date_str}-{time_str}-{sequence:04d}" + @staticmethod + def _items_require_approval(items): + """ + 明细中任一物料命中“出库/借库需审批”→ 需要审批。 + 申请明细只存 name/spec_model,故按 (name, spec_model) 反查启用物料判定。 + """ + from app.models.base import MaterialBase + seen = set() + for item in items: + name = str(item.get('name') or '').strip() + spec = str(item.get('spec_model') or '').strip() + if not name: + continue + key = (name, spec) + if key in seen: + continue + seen.add(key) + hit = MaterialBase.query.filter( + MaterialBase.name == name, + MaterialBase.spec_model == spec, + MaterialBase.is_enabled == True, + MaterialBase.is_approval_required == True + ).first() + if hit: + return True + return False + @staticmethod def submit_approval(applicant_id, items, allowed_approvers, remark=None, approver_id=None, borrower_name=None): @@ -76,30 +103,51 @@ class BorrowApprovalService: except (TypeError, ValueError) as e: raise ValueError(f"第 {idx + 1} 条物品的 quantity 格式无效: {str(e)}") - if not allowed_approvers: - raise ValueError("必须指定至少一位审批人") + # ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过 + need_approval = BorrowApprovalService._items_require_approval(items) + if need_approval and not approver_id: + raise ValueError("该申请包含需审批的物料,请选择审批人后再提交") if approver_id: allowed_approvers = [{"type": "user", "value": int(approver_id)}] + elif not need_approval: + allowed_approvers = [] # 免审批单不绑定审批人 request_no = BorrowApprovalService.generate_request_no() - approval = BorrowApproval( - request_no=request_no, - applicant_id=applicant_id, - remark=remark, - borrower_name=borrower_name, - status=0, # 待审批 - ) + if need_approval: + approval = BorrowApproval( + request_no=request_no, + applicant_id=applicant_id, + remark=remark, + borrower_name=borrower_name, + status=0, # 待审批 + ) + else: + # 默认不审批:创建即已通过(status=1),直接进入“待库管执行” + from datetime import datetime, timezone, timedelta + approval = BorrowApproval( + request_no=request_no, + applicant_id=applicant_id, + remark=remark, + borrower_name=borrower_name, + status=1, + actual_approver_id=applicant_id, + approved_at=datetime.now(timezone(timedelta(hours=8))), + ) approval.set_items(items) - approval.set_allowed_approvers(allowed_approvers) + if allowed_approvers: + approval.set_allowed_approvers(allowed_approvers) + else: + approval.allowed_approvers = '[]' db.session.add(approval) db.session.commit() - # ★ 创建成功后,发送邮件通知审批人(静默处理,不阻断主流程) - BorrowApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id) + # 仅需审批单通知审批人;免审批单静默进入“待库管执行” + if need_approval: + BorrowApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id) return approval diff --git a/inventory-backend/app/services/outbound_service.py b/inventory-backend/app/services/outbound_service.py index 3aa3520..ec68816 100644 --- a/inventory-backend/app/services/outbound_service.py +++ b/inventory-backend/app/services/outbound_service.py @@ -700,6 +700,33 @@ class OutboundApprovalService: return f"APR-OUT-{date_str}-{time_str}-{sequence:04d}" + @staticmethod + def _items_require_approval(items): + """ + 明细中任一物料命中“出库/借库需审批”→ 需要审批。 + 申请明细只存 name/spec_model,故按 (name, spec_model) 反查启用物料判定。 + """ + from app.models.base import MaterialBase + seen = set() + for item in items: + name = str(item.get('name') or '').strip() + spec = str(item.get('spec_model') or '').strip() + if not name: + continue + key = (name, spec) + if key in seen: + continue + seen.add(key) + hit = MaterialBase.query.filter( + MaterialBase.name == name, + MaterialBase.spec_model == spec, + MaterialBase.is_enabled == True, + MaterialBase.is_approval_required == True + ).first() + if hit: + return True + return False + @staticmethod def create_request(applicant_id, items, allowed_approvers, remark=None, approver_id=None, outbound_type=None): """ @@ -745,33 +772,53 @@ class OutboundApprovalService: except (TypeError, ValueError) as e: raise ValueError(f"第 {idx + 1} 条物品的 quantity 格式无效: {str(e)}") - # ★ 校验 allowed_approvers 非空 - if not allowed_approvers: - raise ValueError("必须指定至少一位审批人") + # ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过 + need_approval = OutboundApprovalService._items_require_approval(items) + if need_approval and not approver_id: + raise ValueError("该申请包含需审批的物料,请选择审批人后再提交") # ★ 指定审批人模式:approver_id 覆盖 allowed_approvers if approver_id: allowed_approvers = [{"type": "user", "value": int(approver_id)}] + elif not need_approval: + allowed_approvers = [] # 免审批单不绑定审批人 request_no = OutboundApprovalService.generate_request_no() - approval = OutboundApproval( - request_no=request_no, - applicant_id=applicant_id, - remark=remark, - outbound_type=outbound_type, # 申请时确定的出库类型,扫码出库时带出 - status=0, # 待审批 - ) + if need_approval: + approval = OutboundApproval( + request_no=request_no, + applicant_id=applicant_id, + remark=remark, + outbound_type=outbound_type, # 申请时确定的出库类型,扫码出库时带出 + status=0, # 待审批 + ) + else: + # 默认不审批:创建即已通过(status=1),直接进入“待库管执行” + from datetime import datetime, timezone, timedelta + approval = OutboundApproval( + request_no=request_no, + applicant_id=applicant_id, + remark=remark, + outbound_type=outbound_type, + status=1, + actual_approver_id=applicant_id, + approved_at=datetime.now(timezone(timedelta(hours=8))), + ) # 直接存储前端传来的物料信息快照,不查询/不关联具体库存记录 approval.set_items(items) - approval.set_allowed_approvers(allowed_approvers) + if allowed_approvers: + approval.set_allowed_approvers(allowed_approvers) + else: + approval.allowed_approvers = '[]' db.session.add(approval) db.session.commit() - # ★ 创建成功后,发送邮件通知审批人(精确通知 approver_id 对应的邮箱) - OutboundApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id) + # 仅需审批单通知审批人;免审批单静默进入“待库管执行” + if need_approval: + OutboundApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id) return approval diff --git a/inventory-backend/app/utils/field_permissions.py b/inventory-backend/app/utils/field_permissions.py index f0267eb..fa8d9df 100644 --- a/inventory-backend/app/utils/field_permissions.py +++ b/inventory-backend/app/utils/field_permissions.py @@ -14,6 +14,7 @@ STOCK_FIELD_RBAC_MAPPING = { "category": "material_list:category", "type": "material_list:type", "spec": "material_list:spec", "unit": "material_list:unit", "companyName": "material_list:companyName", "isInspectionRequired": "material_list:isInspectionRequired", + "isApprovalRequired": "material_list:operation", "generalImage": "material_list:files", "generalManual": "material_list:files", "productImageRemark": "material_list:productImageRemark", "manualLinkRemark": "material_list:manualLinkRemark",