from pydantic import BaseModel, Field, 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_price(cls, v): """验证金额必须为非负数""" if v < 0: raise HTTPException(status_code=400, detail='计划完成量不能小于等于0') return v class ConstructionResponse(BaseResponseWithID): """ 施工任务模型,存储在CouchDB中的施工任务文档 """ id : str = Field(..., description="施工任务ID") project_id: str = Field(..., description="所属项目ID", examples=["project123"]) contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"]) show_name: str = Field(..., description="展示名称", examples=["桩基础施工任务"]) construction_section: str = Field(..., description="施工区域", examples=["GF"]) status: Optional[int] = Field(None, description="施工状态", examples=[0]) task_type: str = Field(..., description="施工任务类型", examples=["zjcsgg"]) 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_complete_date: Optional[DateStr] = Field(None, description="实际完工时间", examples=["2024-05-30"]) complete_quantity: float = Field(0.0, description="实际完成量累计", examples=[0.0]) complete_rate: float = Field(0.0, description="实际完成率(%)", examples=[0.0]) contract_quantity: Optional[float] = Field(None, description="设计量", examples=[1000.0]) plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表") statistic_date: Optional[DateStr] = Field(None, description="统计节点时间", examples=["2024-05-31"]) # sort: int = Field(0, description="排序字段", examples=[0]) is_crucial:Optional[bool] = Field(False, description="是否为关键线路") # 2025-10-20新增字段,延迟时间 delay_days: Optional[int] = Field(None, description="延迟时间(天)", examples=[0]) class ConstructionCreate(BaseModel): """ 施工任务创建模型 不对应operation """ project_id: str = Field(..., description="所属项目ID", examples=["project123"]) contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"]) show_name: str = Field(..., description="展示名称", examples=["桩基础施工任务"]) construction_section: str = Field(..., description="施工区域", examples=["GF"]) task_type: str = Field(..., description="施工任务类型", examples=["zjcsgg"]) is_crucial:Optional[bool] = Field(False, description="是否为关键线路") class ConstructionUpdate(BaseModel): """ 施工任务更新模型 CONSTRUCTION_BASE_INFO = "construction.base_info" """ id: str = Field(..., description="施工任务ID") contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"]) show_name: str | None = Field(None, description="展示名称(仅QT类施工可以修改名称)", examples=["桩基础施工任务"]) is_crucial:bool | None = Field(False, description="是否为关键线路") class ConstructionPlanUpdate(BaseModel): """ 施工计划更新模型 对应 CONSTRUCTION_PLAN = "construction.plan" 业务逻辑说明: - API要求计划节点时间必须晚于计划开工日期(不能等于) - 自动清理脏数据:如果 plan_node 有2个或以上节点,删除: 1. 开头节点:如果其 date 等于 plan_start_date 2. 结尾节点:如果其 date 等于 plan_complete_date - 这样避免提交包含边界节点的数据导致API报错 """ id: str = Field(..., description="施工任务ID") plan_start_date: Optional[str] = Field(None, description="计划开工时间", examples=["2024-01-01"]) plan_complete_date: Optional[str] = Field(None, description="计划完工时间", examples=["2024-01-01"]) contract_quantity: float = Field(..., description="设计量") plan_node: Optional[List[PlanNode]] = Field(default_factory=list, description="计划节点列表") @field_validator("contract_quantity") def validate_price(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