Files
X-Financial/server/src/app/services/expense_claim_workflow_repair.py

102 lines
4.4 KiB
Python
Raw Normal View History

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()