102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
|
|
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()
|