from pydantic import Field, BaseModel,field_validator,model_validator from fastapi import HTTPException from typing import List, Optional, Literal from ..common.validators import DateStr from ..common.base import BaseResponseWithID class PlanNode(BaseModel): date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"]) quantity: float = Field(..., description="计划完成量", examples=[100.0]) is_audit: Optional[bool] = Field(False, description="是否已审核", examples=[False]) @field_validator("quantity") def validate_quantity(cls, v): """验证计划完成量必须大于0""" if v < 0: raise HTTPException(status_code=400, detail='计划完成量不能小于0') return v class ContractSettlementResponse(BaseResponseWithID): """ 合同结算返回 """ project_id: str = Field(..., description="所属项目ID", examples=["project123"]) contract_id: str = Field(..., description="所属合同ID", examples=["contract456"]) contract_type: Optional[str] = Field(None, description="合同类型") show_name: Optional[str] = Field(None, description="合同简称", examples=["主变采购合同"]) complete_price: float = Field(0.0, description="实际完成金额", examples=[100000.0]) contract_price: float = Field(0.0, description="合同金额", examples=[120000.0]) actual_complete_date: Optional[DateStr] = Field(None, description="实际结束时间", examples=["2024-06-30"]) actual_start_date: Optional[DateStr] = Field(None, description="实际开始时间", examples=["2024-01-01"]) plan_complete_date: Optional[DateStr] = Field(None, description="计划结束时间", examples=["2024-06-30"]) plan_start_date: Optional[DateStr] = Field(None, description="计划开始时间", examples=["2024-01-01"]) plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表") status: Optional[int] = Field(None,description="供货状态", examples=[0]) # 2025-10-20新增字段,延迟时间 delay_days: Optional[int] = Field(None, description="延迟时间(天)", examples=[0]) statistic_date: Optional[DateStr] = Field( None, description="统计节点截止日期", examples=["2024-05-31"] ) class ContractSettlementCreate(BaseModel): """ 合同结算创建模型 CONTRACT_SETTLEMENT_BASE_INFO = "contract_settlement.base_info" """ project_id: str = Field(..., description="所属项目ID", examples=["project123"]) contract_id: str = Field(..., description="所属合同ID", examples=["contract456"]) show_name: Optional[str] = Field(None, description="合同简称", examples=["主变采购合同"]) class ContractSettlementUpdate(BaseModel): """ 合同结算更新模型 CONTRACT_SETTLEMENT_BASE_INFO = "contract_settlement.base_info" """ id: str = Field(..., description="合同结算ID") contract_id: str = Field(..., description="所属合同ID", examples=["contract456"]) show_name: Optional[str] = Field(None, description="合同简称", examples=["主变采购合同"]) class ContractSettlementPlanUpdate(BaseModel): """ 合同结算计划节点更新模型 CONTRACT_SETTLEMENT_PLAN_NODE = "contract_settlement.plan_node" 开始时间和金额在合同中维护,这里只维护计划节点和计划结束时间 业务逻辑说明: - API要求计划节点时间必须晚于计划开始时间(不能等于) - 自动清理脏数据:如果 plan_node 有2个或以上节点,删除边界节点 """ id: str = Field(..., description="合同结算ID") plan_start_date: Optional[DateStr] = Field(None, exclude=True, description="计划开始时间(不推往后端,仅用于本地首节点清洗)", examples=["2024-06-30"]) plan_complete_date: Optional[DateStr] = Field(None, description="计划结束时间", examples=["2024-06-30"]) plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表") @model_validator(mode='after') def clean_plan_nodes(self): """ 清理计划节点列表中的边界节点 注:settlement的plan_start_date在contract中维护,此处利用外部提供的辅助字段清除非法首尾节点。 """ if not self.plan_node or len(self.plan_node) < 2: return self cleaned_nodes = list(self.plan_node) # 删除开头的边界节点 if (self.plan_start_date and cleaned_nodes and cleaned_nodes[0].date == self.plan_start_date): cleaned_nodes.pop(0) # 删除结尾的边界节点 if (self.plan_complete_date and cleaned_nodes and cleaned_nodes[-1].date == self.plan_complete_date): cleaned_nodes.pop() self.plan_node = cleaned_nodes return self