feat(borrow,outbound): 默认免审批改造——命中物料需审批才走原流程
- models/base.py 加 is_approval_required 列及 isApprovalRequired 序列化;field_permissions 登记 - 新增 POST /inbound/base/batch-approval(批量设需审批,仿批量质检) - borrow_service.submit_approval / outbound_service.create_request:明细含需审批物料→须选审批人走原审批;否则创建即 status=1(待库管执行)、不发审批邮件 - 判定按 (name,spec_model) 反查启用物料
This commit is contained in:
@ -539,6 +539,44 @@ def batch_set_inspection():
|
|||||||
return jsonify({"code": 500, "msg": f"批量设置强制质检失败: {str(e)}"}), 500
|
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)
|
# 2.7 智能分组求最大连号 API (GET /api/v1/inbound/base/spec-latest)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@ -39,6 +39,9 @@ class MaterialBase(db.Model):
|
|||||||
# 强制质检标记(采购入库时必须上传检测报告)
|
# 强制质检标记(采购入库时必须上传检测报告)
|
||||||
is_inspection_required = db.Column(db.Boolean, default=False, comment='是否强制要求质检')
|
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='参考价格')
|
reference_price = db.Column(db.Numeric(10, 2), nullable=True, comment='参考价格')
|
||||||
|
|
||||||
@ -99,6 +102,8 @@ class MaterialBase(db.Model):
|
|||||||
'isEnabled': bool(self.is_enabled),
|
'isEnabled': bool(self.is_enabled),
|
||||||
# 强制质检标记
|
# 强制质检标记
|
||||||
'isInspectionRequired': bool(self.is_inspection_required),
|
'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,
|
'referencePrice': float(self.reference_price) if self.reference_price is not None else None,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,33 @@ class BorrowApprovalService:
|
|||||||
|
|
||||||
return f"APR-BOR-{date_str}-{time_str}-{sequence:04d}"
|
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
|
@staticmethod
|
||||||
def submit_approval(applicant_id, items, allowed_approvers, remark=None, approver_id=None,
|
def submit_approval(applicant_id, items, allowed_approvers, remark=None, approver_id=None,
|
||||||
borrower_name=None):
|
borrower_name=None):
|
||||||
@ -76,30 +103,51 @@ class BorrowApprovalService:
|
|||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
raise ValueError(f"第 {idx + 1} 条物品的 quantity 格式无效: {str(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:
|
if approver_id:
|
||||||
allowed_approvers = [{"type": "user", "value": int(approver_id)}]
|
allowed_approvers = [{"type": "user", "value": int(approver_id)}]
|
||||||
|
elif not need_approval:
|
||||||
|
allowed_approvers = [] # 免审批单不绑定审批人
|
||||||
|
|
||||||
request_no = BorrowApprovalService.generate_request_no()
|
request_no = BorrowApprovalService.generate_request_no()
|
||||||
|
|
||||||
approval = BorrowApproval(
|
if need_approval:
|
||||||
request_no=request_no,
|
approval = BorrowApproval(
|
||||||
applicant_id=applicant_id,
|
request_no=request_no,
|
||||||
remark=remark,
|
applicant_id=applicant_id,
|
||||||
borrower_name=borrower_name,
|
remark=remark,
|
||||||
status=0, # 待审批
|
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_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.add(approval)
|
||||||
db.session.commit()
|
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
|
return approval
|
||||||
|
|
||||||
|
|||||||
@ -700,6 +700,33 @@ class OutboundApprovalService:
|
|||||||
|
|
||||||
return f"APR-OUT-{date_str}-{time_str}-{sequence:04d}"
|
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
|
@staticmethod
|
||||||
def create_request(applicant_id, items, allowed_approvers, remark=None, approver_id=None, outbound_type=None):
|
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:
|
except (TypeError, ValueError) as e:
|
||||||
raise ValueError(f"第 {idx + 1} 条物品的 quantity 格式无效: {str(e)}")
|
raise ValueError(f"第 {idx + 1} 条物品的 quantity 格式无效: {str(e)}")
|
||||||
|
|
||||||
# ★ 校验 allowed_approvers 非空
|
# ★ 需审批判定:明细含“出库/借库需审批”物料 → 走原审批流程;否则默认自动通过
|
||||||
if not allowed_approvers:
|
need_approval = OutboundApprovalService._items_require_approval(items)
|
||||||
raise ValueError("必须指定至少一位审批人")
|
if need_approval and not approver_id:
|
||||||
|
raise ValueError("该申请包含需审批的物料,请选择审批人后再提交")
|
||||||
|
|
||||||
# ★ 指定审批人模式:approver_id 覆盖 allowed_approvers
|
# ★ 指定审批人模式:approver_id 覆盖 allowed_approvers
|
||||||
if approver_id:
|
if approver_id:
|
||||||
allowed_approvers = [{"type": "user", "value": int(approver_id)}]
|
allowed_approvers = [{"type": "user", "value": int(approver_id)}]
|
||||||
|
elif not need_approval:
|
||||||
|
allowed_approvers = [] # 免审批单不绑定审批人
|
||||||
|
|
||||||
request_no = OutboundApprovalService.generate_request_no()
|
request_no = OutboundApprovalService.generate_request_no()
|
||||||
|
|
||||||
approval = OutboundApproval(
|
if need_approval:
|
||||||
request_no=request_no,
|
approval = OutboundApproval(
|
||||||
applicant_id=applicant_id,
|
request_no=request_no,
|
||||||
remark=remark,
|
applicant_id=applicant_id,
|
||||||
outbound_type=outbound_type, # 申请时确定的出库类型,扫码出库时带出
|
remark=remark,
|
||||||
status=0, # 待审批
|
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_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.add(approval)
|
||||||
db.session.commit()
|
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
|
return approval
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ STOCK_FIELD_RBAC_MAPPING = {
|
|||||||
"category": "material_list:category", "type": "material_list:type",
|
"category": "material_list:category", "type": "material_list:type",
|
||||||
"spec": "material_list:spec", "unit": "material_list:unit",
|
"spec": "material_list:spec", "unit": "material_list:unit",
|
||||||
"companyName": "material_list:companyName", "isInspectionRequired": "material_list:isInspectionRequired",
|
"companyName": "material_list:companyName", "isInspectionRequired": "material_list:isInspectionRequired",
|
||||||
|
"isApprovalRequired": "material_list:operation",
|
||||||
"generalImage": "material_list:files", "generalManual": "material_list:files",
|
"generalImage": "material_list:files", "generalManual": "material_list:files",
|
||||||
"productImageRemark": "material_list:productImageRemark",
|
"productImageRemark": "material_list:productImageRemark",
|
||||||
"manualLinkRemark": "material_list:manualLinkRemark",
|
"manualLinkRemark": "material_list:manualLinkRemark",
|
||||||
|
|||||||
Reference in New Issue
Block a user