feat(claim): 重构报销审批流并收敛风险标记

- 直属领导兼任部门 P8 预算审批人时合并预算审批,直接流转至财务审批
- 预算超过警戒值时强制要求预算管理者填写审批意见
- 新增风险标记去重工具,消除各审核阶段重复风险卡片
- 新增工作流修复 Mixin,纠正重复预算审批阶段的历史数据
- 收紧单据删除权限至 admin,放宽预算分析可见范围至当前审核人
- 提交校验放宽已上传票据条目的 OCR 字段缺失并忽略尾部占位条目
This commit is contained in:
caoxiaozhu
2026-06-17 14:38:07 +08:00
parent 09a66c72cb
commit 1f4681f486
11 changed files with 372 additions and 27 deletions

View File

@@ -641,6 +641,12 @@ class ExpenseClaimItemSyncMixin:
issues: list[str] = []
claim_location_required = self._is_location_required_expense_type(claim.expense_type)
claim_min_attachment_count = self._resolve_claim_required_attachment_count(claim)
substantive_items = [
item
for item in list(claim.items or [])
if str(item.item_type or "").strip().lower() not in SYSTEM_GENERATED_ITEM_TYPES
and not self._is_submission_placeholder_item(item)
]
if self._is_missing_value(claim.employee_name):
issues.append("申请人未完善")
@@ -658,28 +664,39 @@ class ExpenseClaimItemSyncMixin:
issues.append("发生时间未完善")
if int(claim.invoice_count or 0) < claim_min_attachment_count:
issues.append("票据附件数量不足")
if not claim.items:
if not substantive_items:
issues.append("费用明细不能为空")
for index, item in enumerate(claim.items, start=1):
prefix = f"费用明细第 {index}"
is_system_generated = str(item.item_type or "").strip().lower() in SYSTEM_GENERATED_ITEM_TYPES
if is_system_generated or self._is_submission_placeholder_item(item):
continue
item_location_required = self._is_location_required_expense_type(item.item_type or claim.expense_type)
if item.item_date is None:
item_has_attachment = not self._is_missing_value(item.invoice_id)
if not item_has_attachment and item.item_date is None:
issues.append(f"{prefix}缺少日期")
if self._is_missing_value(item.item_type):
issues.append(f"{prefix}缺少费用项目")
if self._is_missing_value(item.item_reason):
if not item_has_attachment and self._is_missing_value(item.item_reason):
issues.append(f"{prefix}缺少说明")
if item_location_required and self._is_missing_value(item.item_location):
if not item_has_attachment and item_location_required and self._is_missing_value(item.item_location):
issues.append(f"{prefix}缺少地点")
if item.item_amount is None or item.item_amount <= Decimal("0.00"):
if not item_has_attachment and (item.item_amount is None or item.item_amount <= Decimal("0.00")):
issues.append(f"{prefix}缺少金额")
if self._is_attachment_required_item_type(item.item_type) and self._is_missing_value(item.invoice_id):
if self._is_attachment_required_item_type(item.item_type) and not item_has_attachment:
issues.append(f"{prefix}缺少票据标识")
return issues
def _is_submission_placeholder_item(self, item: ExpenseClaimItem) -> bool:
if not self._is_missing_value(item.invoice_id):
return False
missing_reason = self._is_missing_value(item.item_reason)
missing_location = self._is_missing_value(item.item_location)
missing_amount = item.item_amount is None or item.item_amount <= Decimal("0.00")
return missing_reason and missing_location and missing_amount
def _is_location_required_expense_type(self, expense_type: str | None) -> bool:
policy = self._get_expense_scene_policy(expense_type)
if policy is None: