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

133 lines
6.4 KiB
Python
Raw Normal View History

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(),
"application_lodging_daily_cap": str(detail.get("lodging_daily_cap") or "").strip(),
"application_subsidy_daily_cap": str(detail.get("subsidy_daily_cap") or "").strip(),
"application_transport_policy": str(detail.get("transport_policy") or "").strip(),
"application_policy_estimate": str(detail.get("policy_estimate") or "").strip(),
"application_rule_name": str(detail.get("rule_name") or "").strip(),
"application_rule_version": str(detail.get("rule_version") 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