2026-05-06 17:43:47 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-05-13 03:22:52 +00:00
|
|
|
from datetime import date, datetime
|
2026-05-06 17:43:47 +08:00
|
|
|
from decimal import Decimal
|
2026-05-13 03:22:52 +00:00
|
|
|
from typing import Any
|
2026-05-06 17:43:47 +08:00
|
|
|
|
2026-06-09 08:32:00 +00:00
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
|
|
|
|
from app.services.expense_claim_budget_risk_flags import dedupe_budget_risk_flags
|
2026-05-06 17:43:47 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReimbursementCreate(BaseModel):
|
|
|
|
|
request_no: str
|
|
|
|
|
employee_id: str
|
|
|
|
|
title: str
|
|
|
|
|
category: str
|
|
|
|
|
amount: Decimal
|
|
|
|
|
reason: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReimbursementRead(BaseModel):
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
request_no: str
|
|
|
|
|
employee_id: str
|
|
|
|
|
title: str
|
|
|
|
|
category: str
|
|
|
|
|
status: str
|
|
|
|
|
amount: Decimal
|
|
|
|
|
reason: str | None
|
|
|
|
|
created_at: datetime
|
|
|
|
|
updated_at: datetime
|
2026-05-13 03:22:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseClaimItemRead(BaseModel):
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
item_date: date
|
|
|
|
|
item_type: str
|
|
|
|
|
item_reason: str
|
|
|
|
|
item_location: str
|
2026-06-03 15:46:56 +08:00
|
|
|
item_note: str = ""
|
2026-05-13 03:22:52 +00:00
|
|
|
item_amount: Decimal
|
|
|
|
|
invoice_id: str | None
|
2026-05-21 09:28:33 +08:00
|
|
|
is_system_generated: bool = False
|
2026-05-13 03:22:52 +00:00
|
|
|
created_at: datetime
|
|
|
|
|
updated_at: datetime
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 06:45:04 +00:00
|
|
|
class ExpenseClaimAttachmentAnalysisRead(BaseModel):
|
|
|
|
|
severity: str
|
|
|
|
|
label: str
|
|
|
|
|
headline: str
|
|
|
|
|
summary: str
|
|
|
|
|
points: list[str] = Field(default_factory=list)
|
2026-05-21 23:52:34 +08:00
|
|
|
rule_basis: list[str] = Field(default_factory=list)
|
2026-05-13 06:45:04 +00:00
|
|
|
suggestion: str = ""
|
|
|
|
|
|
|
|
|
|
|
2026-05-14 09:32:36 +00:00
|
|
|
class ExpenseClaimAttachmentDocumentFieldRead(BaseModel):
|
|
|
|
|
key: str
|
|
|
|
|
label: str
|
|
|
|
|
value: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseClaimAttachmentDocumentInfoRead(BaseModel):
|
|
|
|
|
document_type: str = "other"
|
|
|
|
|
document_type_label: str = "其他单据"
|
|
|
|
|
scene_code: str = "other"
|
|
|
|
|
scene_label: str = "其他票据"
|
|
|
|
|
fields: list[ExpenseClaimAttachmentDocumentFieldRead] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseClaimAttachmentRequirementRead(BaseModel):
|
|
|
|
|
matches: bool = False
|
|
|
|
|
current_expense_type: str = "other"
|
|
|
|
|
current_expense_type_label: str = "其他"
|
|
|
|
|
allowed_scene_labels: list[str] = Field(default_factory=list)
|
|
|
|
|
recognized_scene_code: str = "other"
|
|
|
|
|
recognized_scene_label: str = "其他票据"
|
|
|
|
|
recognized_document_type: str = "other"
|
|
|
|
|
recognized_document_type_label: str = "其他单据"
|
|
|
|
|
message: str = ""
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 06:45:04 +00:00
|
|
|
class ExpenseClaimAttachmentRead(BaseModel):
|
|
|
|
|
file_name: str
|
|
|
|
|
storage_key: str
|
|
|
|
|
media_type: str
|
|
|
|
|
size_bytes: int
|
|
|
|
|
uploaded_at: datetime | None = None
|
|
|
|
|
previewable: bool = True
|
2026-05-14 15:42:45 +00:00
|
|
|
preview_kind: str = ""
|
|
|
|
|
preview_url: str = ""
|
2026-05-13 06:45:04 +00:00
|
|
|
analysis: ExpenseClaimAttachmentAnalysisRead | None = None
|
2026-05-14 09:32:36 +00:00
|
|
|
document_info: ExpenseClaimAttachmentDocumentInfoRead | None = None
|
|
|
|
|
requirement_check: ExpenseClaimAttachmentRequirementRead | None = None
|
2026-05-13 06:45:04 +00:00
|
|
|
|
|
|
|
|
|
2026-05-13 03:22:52 +00:00
|
|
|
class ExpenseClaimItemUpdate(BaseModel):
|
|
|
|
|
item_date: date | None = None
|
|
|
|
|
item_type: str | None = None
|
|
|
|
|
item_reason: str | None = None
|
|
|
|
|
item_location: str | None = None
|
2026-06-03 15:46:56 +08:00
|
|
|
item_note: str | None = None
|
2026-05-13 03:22:52 +00:00
|
|
|
item_amount: Decimal | None = None
|
|
|
|
|
invoice_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 06:45:04 +00:00
|
|
|
class ExpenseClaimItemCreate(BaseModel):
|
|
|
|
|
item_date: date | None = None
|
|
|
|
|
item_type: str | None = None
|
|
|
|
|
item_reason: str | None = None
|
|
|
|
|
item_location: str | None = None
|
2026-06-03 15:46:56 +08:00
|
|
|
item_note: str | None = None
|
2026-05-13 06:45:04 +00:00
|
|
|
item_amount: Decimal | None = None
|
|
|
|
|
invoice_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 10:57:06 +08:00
|
|
|
class ExpenseClaimUpdate(BaseModel):
|
|
|
|
|
reason: str | None = Field(default=None, max_length=500)
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 17:31:40 +08:00
|
|
|
class ExpenseClaimStandardAdjustmentRisk(BaseModel):
|
|
|
|
|
risk_id: str | None = Field(default=None, max_length=120)
|
|
|
|
|
item_id: str | None = Field(default=None, max_length=120)
|
|
|
|
|
title: str | None = Field(default=None, max_length=120)
|
|
|
|
|
risk: str | None = Field(default=None, max_length=500)
|
2026-06-04 11:03:29 +08:00
|
|
|
application_days: int | None = Field(default=None, ge=1, le=365)
|
2026-06-03 17:31:40 +08:00
|
|
|
original_amount: Decimal | None = None
|
|
|
|
|
reimbursable_amount: Decimal | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseClaimStandardAdjustmentPayload(BaseModel):
|
|
|
|
|
risks: list[ExpenseClaimStandardAdjustmentRisk] = Field(default_factory=list, max_length=20)
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 03:22:52 +00:00
|
|
|
class ExpenseClaimRead(BaseModel):
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
claim_no: str
|
|
|
|
|
employee_id: str | None
|
|
|
|
|
employee_name: str
|
|
|
|
|
department_id: str | None
|
|
|
|
|
department_name: str
|
2026-05-13 06:54:27 +00:00
|
|
|
employee_position: str | None = None
|
|
|
|
|
employee_grade: str | None = None
|
|
|
|
|
manager_name: str | None = None
|
2026-06-09 08:32:00 +00:00
|
|
|
finance_owner_name: str | None = None
|
|
|
|
|
finance_approver_name: str | None = None
|
2026-06-01 17:07:14 +08:00
|
|
|
budget_approver_name: str | None = None
|
|
|
|
|
budget_approver_grade: str | None = None
|
|
|
|
|
budget_approver_role_code: str | None = None
|
2026-05-13 06:54:27 +00:00
|
|
|
role_labels: list[str] = Field(default_factory=list)
|
2026-05-13 03:22:52 +00:00
|
|
|
project_code: str | None
|
|
|
|
|
expense_type: str
|
|
|
|
|
reason: str
|
|
|
|
|
location: str
|
|
|
|
|
amount: Decimal
|
|
|
|
|
currency: str
|
|
|
|
|
invoice_count: int
|
|
|
|
|
occurred_at: datetime
|
|
|
|
|
submitted_at: datetime | None
|
|
|
|
|
status: str
|
|
|
|
|
approval_stage: str | None
|
|
|
|
|
risk_flags_json: list[Any] = Field(default_factory=list)
|
|
|
|
|
created_at: datetime
|
|
|
|
|
updated_at: datetime
|
|
|
|
|
items: list[ExpenseClaimItemRead] = Field(default_factory=list)
|
|
|
|
|
|
2026-06-09 08:32:00 +00:00
|
|
|
@field_validator("risk_flags_json", mode="before")
|
|
|
|
|
@classmethod
|
|
|
|
|
def dedupe_budget_risk_flags_for_read(cls, value: Any) -> list[Any]:
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
return dedupe_budget_risk_flags(value)
|
|
|
|
|
return []
|
|
|
|
|
|
2026-05-13 03:22:52 +00:00
|
|
|
|
|
|
|
|
class ExpenseClaimActionResponse(BaseModel):
|
|
|
|
|
message: str
|
|
|
|
|
claim_id: str
|
|
|
|
|
status: str | None = None
|
2026-05-13 06:45:04 +00:00
|
|
|
|
|
|
|
|
|
2026-06-20 14:41:59 +08:00
|
|
|
class ExpenseApplicationPreviewActionPayload(BaseModel):
|
|
|
|
|
source: str = Field(default="user_message", max_length=80)
|
|
|
|
|
user_id: str | None = Field(default=None, max_length=120)
|
|
|
|
|
conversation_id: str | None = Field(default=None, max_length=120)
|
|
|
|
|
message: str = Field(min_length=1, max_length=4000)
|
|
|
|
|
context_json: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseApplicationPreviewActionResult(BaseModel):
|
|
|
|
|
message: str
|
|
|
|
|
answer: str
|
|
|
|
|
suggested_actions: list[dict[str, Any]] = Field(default_factory=list)
|
|
|
|
|
risk_flags: list[str] = Field(default_factory=list)
|
|
|
|
|
requires_confirmation: bool = False
|
|
|
|
|
draft_payload: dict[str, Any] | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseApplicationPreviewActionResponse(BaseModel):
|
|
|
|
|
status: str = "succeeded"
|
|
|
|
|
conversation_id: str | None = None
|
|
|
|
|
result: ExpenseApplicationPreviewActionResult
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 14:21:56 +08:00
|
|
|
class ExpenseClaimReturnPayload(BaseModel):
|
|
|
|
|
reason: str | None = Field(default=None, max_length=500)
|
2026-05-20 21:00:47 +08:00
|
|
|
reason_codes: list[str] = Field(default_factory=list, max_length=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseClaimApprovalPayload(BaseModel):
|
|
|
|
|
opinion: str | None = Field(default=None, max_length=500)
|
2026-05-20 14:21:56 +08:00
|
|
|
|
|
|
|
|
|
2026-05-21 09:28:33 +08:00
|
|
|
class TravelReimbursementCalculatorRequest(BaseModel):
|
|
|
|
|
days: int = Field(ge=1, le=365)
|
|
|
|
|
location: str = Field(min_length=1, max_length=120)
|
|
|
|
|
grade: str | None = Field(default=None, max_length=30)
|
2026-06-15 22:55:18 +08:00
|
|
|
transport_mode: str | None = Field(default=None, max_length=30)
|
|
|
|
|
origin_location: str | None = Field(default=None, max_length=120)
|
|
|
|
|
travel_date: date | None = None
|
2026-05-21 09:28:33 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TravelReimbursementCalculatorResponse(BaseModel):
|
|
|
|
|
days: int
|
|
|
|
|
location: str
|
|
|
|
|
matched_city: str
|
|
|
|
|
city_tier: str
|
|
|
|
|
grade: str
|
|
|
|
|
grade_band: str
|
|
|
|
|
grade_band_label: str
|
|
|
|
|
hotel_rate: Decimal
|
|
|
|
|
hotel_amount: Decimal
|
|
|
|
|
allowance_region: str
|
|
|
|
|
meal_allowance_rate: Decimal
|
|
|
|
|
basic_allowance_rate: Decimal
|
|
|
|
|
total_allowance_rate: Decimal
|
|
|
|
|
allowance_amount: Decimal
|
2026-06-15 22:55:18 +08:00
|
|
|
transport_mode: str = ""
|
|
|
|
|
transport_origin: str = ""
|
|
|
|
|
transport_destination: str = ""
|
|
|
|
|
transport_estimated_amount: Decimal = Decimal("0.00")
|
|
|
|
|
transport_estimate_basis: str = ""
|
|
|
|
|
transport_estimate_confidence: str = ""
|
|
|
|
|
transport_estimate_source: str = ""
|
|
|
|
|
transport_estimate_rule_code: str = ""
|
|
|
|
|
transport_estimate_rule_name: str = ""
|
|
|
|
|
transport_estimate_rule_version: str = ""
|
|
|
|
|
travel_date: date | None = None
|
2026-05-21 09:28:33 +08:00
|
|
|
total_amount: Decimal
|
|
|
|
|
rule_name: str
|
|
|
|
|
rule_version: str
|
|
|
|
|
formula_text: str
|
|
|
|
|
summary_text: str
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 06:45:04 +00:00
|
|
|
class ExpenseClaimAttachmentActionResponse(BaseModel):
|
|
|
|
|
message: str
|
|
|
|
|
claim_id: str
|
|
|
|
|
item_id: str
|
|
|
|
|
invoice_id: str | None = None
|
2026-05-21 23:52:34 +08:00
|
|
|
item_date: date | None = None
|
|
|
|
|
item_type: str | None = None
|
|
|
|
|
item_reason: str | None = None
|
|
|
|
|
item_location: str | None = None
|
2026-06-03 15:46:56 +08:00
|
|
|
item_note: str | None = None
|
2026-05-21 09:28:33 +08:00
|
|
|
item_amount: Decimal | None = None
|
|
|
|
|
claim_amount: Decimal | None = None
|
2026-06-01 17:07:14 +08:00
|
|
|
claim_risk_flags: list[Any] = Field(default_factory=list)
|
2026-05-13 06:45:04 +00:00
|
|
|
attachment: ExpenseClaimAttachmentRead | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpenseClaimItemActionResponse(BaseModel):
|
|
|
|
|
message: str
|
|
|
|
|
claim_id: str
|
|
|
|
|
item_id: str
|