feat(claim): 重构报销审批流并收敛风险标记
- 直属领导兼任部门 P8 预算审批人时合并预算审批,直接流转至财务审批 - 预算超过警戒值时强制要求预算管理者填写审批意见 - 新增风险标记去重工具,消除各审核阶段重复风险卡片 - 新增工作流修复 Mixin,纠正重复预算审批阶段的历史数据 - 收紧单据删除权限至 admin,放宽预算分析可见范围至当前审核人 - 提交校验放宽已上传票据条目的 OCR 字段缺失并忽略尾部占位条目
This commit is contained in:
101
server/src/app/services/expense_claim_workflow_repair.py
Normal file
101
server/src/app/services/expense_claim_workflow_repair.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from app.models.financial_record import ExpenseClaim
|
||||
from app.services.expense_claim_risk_stage import risk_business_stage_for_claim, with_risk_business_stage
|
||||
from app.services.expense_claim_workflow_constants import (
|
||||
BUDGET_MANAGER_APPROVAL_STAGE,
|
||||
DIRECT_MANAGER_APPROVAL_STAGE,
|
||||
FINANCE_APPROVAL_STAGE,
|
||||
)
|
||||
|
||||
|
||||
class ExpenseClaimWorkflowRepairMixin:
|
||||
def _repair_duplicate_budget_approval_stages(self, claims: list[ExpenseClaim]) -> None:
|
||||
repaired_claims = [
|
||||
claim
|
||||
for claim in claims
|
||||
if claim is not None and self._repair_duplicate_budget_approval_stage(claim)
|
||||
]
|
||||
if not repaired_claims:
|
||||
return
|
||||
|
||||
self.db.commit()
|
||||
for claim in repaired_claims:
|
||||
self.db.refresh(claim)
|
||||
|
||||
def _repair_duplicate_budget_approval_stage(self, claim: ExpenseClaim) -> bool:
|
||||
if self._is_expense_application_claim(claim):
|
||||
return False
|
||||
if str(claim.status or "").strip().lower() != "submitted":
|
||||
return False
|
||||
if str(claim.approval_stage or "").strip() != BUDGET_MANAGER_APPROVAL_STAGE:
|
||||
return False
|
||||
if self._has_duplicate_budget_stage_repair_flag(claim):
|
||||
return False
|
||||
|
||||
approval_event = self._find_duplicate_budget_handoff_event(claim)
|
||||
if approval_event is None:
|
||||
return False
|
||||
|
||||
claim.approval_stage = FINANCE_APPROVAL_STAGE
|
||||
claim.risk_flags_json = [
|
||||
*list(claim.risk_flags_json or []),
|
||||
self._build_duplicate_budget_stage_repair_flag(approval_event),
|
||||
]
|
||||
return True
|
||||
|
||||
def _find_duplicate_budget_handoff_event(self, claim: ExpenseClaim) -> dict[str, Any] | None:
|
||||
flags = [
|
||||
flag
|
||||
for flag in list(claim.risk_flags_json or [])
|
||||
if isinstance(flag, dict)
|
||||
and str(flag.get("source") or "").strip() == "manual_approval"
|
||||
and str(flag.get("event_type") or "").strip() == "expense_claim_approval"
|
||||
and str(flag.get("previous_approval_stage") or "").strip() == DIRECT_MANAGER_APPROVAL_STAGE
|
||||
and str(flag.get("next_approval_stage") or "").strip() == BUDGET_MANAGER_APPROVAL_STAGE
|
||||
]
|
||||
for flag in reversed(flags):
|
||||
operator = self._normalize_repair_identity(flag.get("operator"))
|
||||
next_approver_name = self._normalize_repair_identity(flag.get("next_approver_name"))
|
||||
if operator and next_approver_name and operator == next_approver_name:
|
||||
return flag
|
||||
|
||||
budget_manager = self._access_policy.resolve_department_budget_manager(claim)
|
||||
if budget_manager is None:
|
||||
continue
|
||||
if operator and operator == self._normalize_repair_identity(budget_manager.name):
|
||||
return flag
|
||||
return None
|
||||
|
||||
def _has_duplicate_budget_stage_repair_flag(self, claim: ExpenseClaim) -> bool:
|
||||
return any(
|
||||
isinstance(flag, dict)
|
||||
and str(flag.get("source") or "").strip() == "approval_flow_repair"
|
||||
and str(flag.get("event_type") or "").strip() == "duplicate_budget_approval_stage_repaired"
|
||||
for flag in list(claim.risk_flags_json or [])
|
||||
)
|
||||
|
||||
def _build_duplicate_budget_stage_repair_flag(self, approval_event: dict[str, Any]) -> dict[str, Any]:
|
||||
return with_risk_business_stage(
|
||||
{
|
||||
"source": "approval_flow_repair",
|
||||
"event_type": "duplicate_budget_approval_stage_repaired",
|
||||
"severity": "info",
|
||||
"label": "重复预算审批已跳过",
|
||||
"message": "系统识别直属领导与预算管理者为同一人,已跳过重复预算审批并流转至财务审批。",
|
||||
"previous_approval_stage": BUDGET_MANAGER_APPROVAL_STAGE,
|
||||
"next_approval_stage": FINANCE_APPROVAL_STAGE,
|
||||
"related_approval_event_id": approval_event.get("approval_event_id"),
|
||||
"budget_approval_merged": True,
|
||||
"budget_approval_merged_reason": "direct_manager_is_department_budget_approver",
|
||||
"created_at": datetime.now(UTC).isoformat(),
|
||||
},
|
||||
risk_business_stage_for_claim(is_application_claim=False),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_repair_identity(value: Any) -> str:
|
||||
return str(value or "").strip().lower()
|
||||
Reference in New Issue
Block a user