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