from __future__ import annotations import uuid from datetime import UTC, datetime from decimal import Decimal from typing import Any from app.models.financial_record import ExpenseClaim APPLICATION_REIMBURSEMENT_TYPE_MAP = { "travel_application": "travel", "purchase_application": "office", "meeting_application": "meeting", "expense_application": "other", "application": "other", } class ExpenseClaimApplicationHandoffMixin: @staticmethod def _resolve_reimbursement_type_from_application(expense_type: str | None) -> str: normalized = str(expense_type or "").strip().lower() if normalized in APPLICATION_REIMBURSEMENT_TYPE_MAP: return APPLICATION_REIMBURSEMENT_TYPE_MAP[normalized] if normalized.endswith("_application"): return normalized.removesuffix("_application") or "other" return normalized or "other" @staticmethod def _resolve_application_detail(application_claim: ExpenseClaim) -> dict[str, str]: for flag in list(application_claim.risk_flags_json or []): if not isinstance(flag, dict) or str(flag.get("source") or "").strip() != "application_detail": continue detail = flag.get("application_detail") or flag.get("applicationDetail") or {} if isinstance(detail, dict): return {str(key): str(value or "").strip() for key, value in detail.items()} return {} @staticmethod def _build_application_handoff_detail(application_claim: ExpenseClaim) -> dict[str, str]: detail = ExpenseClaimApplicationHandoffMixin._resolve_application_detail(application_claim) application_time = str(detail.get("time") or "").strip() if not application_time and application_claim.occurred_at is not None: application_time = application_claim.occurred_at.isoformat() application_amount = str(detail.get("amount") or "").strip() if not application_amount: application_amount = str(application_claim.amount or Decimal("0.00")) return { "application_type": str(detail.get("application_type") or application_claim.expense_type or "").strip(), "application_content": " / ".join( item for item in [ str(detail.get("application_type") or application_claim.expense_type or "").strip(), str(detail.get("location") or application_claim.location or "").strip(), ] if item ), "application_reason": str(detail.get("reason") or application_claim.reason or "").strip(), "application_days": str(detail.get("days") or "").strip(), "application_location": str(detail.get("location") or application_claim.location or "").strip(), "application_amount": application_amount, "application_time": application_time, "application_transport_mode": str(detail.get("transport_mode") or "").strip(), } def _create_reimbursement_draft_from_application( self, *, application_claim: ExpenseClaim, approval_flag: dict[str, Any], operator: str, ) -> ExpenseClaim: occurred_at = application_claim.occurred_at or datetime.now(UTC) created_at = datetime.now(UTC) draft_claim = ExpenseClaim( claim_no=self._generate_claim_no(occurred_at), employee_id=application_claim.employee_id, employee_name=application_claim.employee_name, department_id=application_claim.department_id, department_name=application_claim.department_name, project_code=application_claim.project_code, expense_type=self._resolve_reimbursement_type_from_application(application_claim.expense_type), reason=application_claim.reason, location=application_claim.location, amount=application_claim.amount or Decimal("0.00"), currency=application_claim.currency or "CNY", invoice_count=0, occurred_at=occurred_at, submitted_at=None, status="draft", approval_stage="待提交", risk_flags_json=[ { "source": "application_handoff", "event_type": "expense_application_to_reimbursement_draft", "handoff_event_id": str(uuid.uuid4()), "severity": "info", "label": "申请转报销草稿", "message": ( f"费用申请 {application_claim.claim_no} 已由 {operator} 确认审核," "系统已生成报销草稿。" ), "application_claim_id": application_claim.id, "application_claim_no": application_claim.claim_no, "application_budget_amount": str(application_claim.amount or Decimal("0.00")), "application_detail": self._build_application_handoff_detail(application_claim), "application_approval_event_id": str(approval_flag.get("approval_event_id") or ""), "leader_opinion": str( approval_flag.get("leader_opinion") or approval_flag.get("opinion") or "" ).strip(), "budget_opinion": str(approval_flag.get("budget_opinion") or "").strip(), "created_at": created_at.isoformat(), } ], ) self.db.add(draft_claim) self.db.flush() approval_flag["generated_draft_claim_id"] = draft_claim.id approval_flag["generated_draft_claim_no"] = draft_claim.claim_no approval_flag["handoff_event_type"] = "expense_application_to_reimbursement_draft" approval_flag["handoff_message"] = f"已生成报销草稿 {draft_claim.claim_no}。" return draft_claim