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): """验证实际完成量累计必须为非负数""" if v < 0: raise HTTPException(status_code=400, detail='计划完成量不能小于等于0') return v class MaterialResponse(BaseResponseWithID): """ 供货模型 """ id : str = Field(..., description="物资ID", examples=["material123"]) project_id: str = Field(..., description="所属项目ID", examples=["project123"]) contract_id: str = Field("", description="所属合同ID", examples=["contract456"]) show_name: str = Field("", description="展示名称", examples=["主变"]) material_type: str = Field("", description="物资类型") contract_quantity: float = Field(0.0, description="合同数量", examples=[1000.0]) plan_start_quantity: float = Field( 0.0, description="计划首批到货量", examples=[1000.0] ) complete_quantity: float = Field(0.0, description="实际到货累计", examples=[0.0]) complete_rate: float = Field(0.0, description="实际到货率(%)", examples=[0.0]) plan_start_date: Optional[DateStr] = Field( None, description="计划首批到货时间", examples=["2024-01-01"] ) plan_complete_date: Optional[DateStr] = Field( None, description="计划全部到货时间", examples=["2024-06-01"] ) actual_start_date: Optional[DateStr] = Field( None, description="实际首批到货时间", examples=["2024-01-15"] ) actual_complete_date: Optional[DateStr] = Field( None, description="实际全部到货时间", examples=["2024-06-10"] ) 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 MaterialCreate(BaseModel): """ 创建物资 MATERIAL_BASE_INFO = "material.base_info" """ project_id: str = Field(..., description="所属项目ID", examples=["project123"]) contract_id: str = Field(..., description="所属合同ID", examples=["contract456"]) show_name: str = Field(..., description="展示名称", examples=["主变"]) material_type: str = Field(..., description="物资类型") contract_quantity: float = Field(0.0, description="合同数量", examples=[1000.0]) @field_validator("contract_quantity") def validate_quantity(cls, v): """验证实际完成量累计必须为非负数""" if v < 0: raise HTTPException(status_code=400, detail='合同数量不能小于0') return v class MaterialUpdate(BaseModel): """ 更新物资 MATERIAL_BASE_INFO = "material.base_info" """ id: str = Field(..., description="物资ID", examples=["material123"]) contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"]) show_name: Optional[str] = Field(None, description="展示名称", examples=["主变"]) class MaterialPlanUpdate(BaseModel): """ 物资计划更新模型 MATERIAL_PLAN = "material.plan" 业务逻辑说明: - API要求计划节点时间必须晚于计划首批到货时间(不能等于) - 自动清理脏数据:如果 plan_node 有2个或以上节点,删除: 1. 开头节点:如果其 date 等于 plan_start_date 2. 结尾节点:如果其 date 等于 plan_complete_date - 这样避免提交包含边界节点的数据导致API报错 """ id: str = Field(..., description="物资ID", examples=["material123"]) plan_start_quantity: Optional[float] = Field( None, description="计划首批到货量", examples=[1000.0] ) plan_start_date: Optional[str] = Field(None, description="计划首批到货时间", examples=["2024-01-01"]) plan_complete_date: Optional[str] = Field(None, description="计划全部到货时间", examples=["2024-06-01"]) contract_quantity: float = Field(..., description="合同数量", examples=[1000.0]) plan_node: Optional[List[PlanNode]] = Field(default_factory=list, description="计划节点列表") @field_validator("contract_quantity") def validate_quantity(cls, v): """验证实际完成量累计必须为非负数""" if v < 0: raise HTTPException(status_code=400, detail='合同数量不能小于0') return v @field_validator("plan_start_quantity") def validate_plan_start_quantity(cls, v): """验证实际完成量累计必须为非负数""" if v < 0: raise HTTPException(status_code=400, detail='计划首批到货量不能小于0') return v @model_validator(mode='after') def clean_plan_nodes(self): """ 清理计划节点列表中的边界节点 如果 plan_node 有2个或以上节点,删除: - 开头节点:date 等于 plan_start_date - 结尾节点:date 等于 plan_complete_date 这避免了API报错“计划节点时间必须晚于计划首批到货时间” """ 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