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 PowerResponse(BaseResponseWithID): """ 力能模型 """ code: Optional[str] = Field( "personnel", description="力能类型(人员模块拓展为力能模块,可以在本字段保存人员/机具等编码)", examples=["personnel"], ) project_id: str = Field(..., description="所属项目", examples=["project123"]) contract_id: Optional[str] = Field( None, description="所属合同(目前前端页面显示没有按这个字段,仅通过脚本绑定了epc合同,用于接口数据推送。后续行为待讨论)", examples=["contract456"], ) show_name: str = Field("", description="展示名称", examples=["施工班组A"]) status: Optional[int] = Field(None, description="人员状态", examples=[0]) plan_attendance: int = Field( 0, description="计划出勤人数(暂时定为最后一次报明细时的计划出勤人数)", examples=[10], ) actual_daily_attendance: int = Field(0, description="实际日均出勤", examples=[8]) attendance_rate: float = Field(0.0, description="出勤率", examples=[0.8]) statistic_date: Optional[DateStr] = Field( None, description="统计节点", examples=["2024-05-31"] ) plan_exit_date: Optional[DateStr] = Field( None, description="计划退场时间", examples=["2024-06-30"] ) actual_exit_date: Optional[DateStr] = Field( None, description="实际退场时间", examples=["2024-07-05"] ) plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表") plan_start_date: Optional[DateStr] = Field( None, description="计划首批到场时间", examples=["2024-05-01"] ) plan_start_quantity: int = Field(0, description="计划首批到场人数", examples=[5], ge=0) actual_start_date: Optional[DateStr] = Field( None, description="实际首批到场时间", examples=["2024-05-02"] ) actual_start_quantity: Optional[int] = Field( None, description="实际首批到场人数", examples=[4] ) class PowerCreate(BaseModel): """ 创建力能 POWER_BASE_INFO = "personnel.base_info" """ project_id: str = Field(..., description="所属项目ID", examples=["project123"]) contract_id: Optional[str] = Field( None, description="所属合同ID", examples=["contract456"] ) #show_name: str = Field(..., description="展示名称", examples=["施工班组A"]) code: Optional[str] = Field( "personnel", description="力能类型(人员模块拓展为力能模块,可以在本字段保存人员/机具等编码)", examples=["personnel"], ) class PowerUpdate(BaseModel): """ 更新力能 POWER_BASE_INFO = "personnel.base_info" """ id: str = Field(..., description="力能ID") contract_id: Optional[str] = Field( None, description="所属合同ID", examples=["contract456"] ) #show_name: Optional[str] = Field(None, description="展示名称", examples=["施工班组A"]) class PowerPlanUpdate(BaseModel): """ 力能计划更新模型 POWER_PLAN = "personnel.plan" 业务逻辑说明: - API要求计划节点时间必须晚于计划首批到场时间(不能等于) - 自动清理脏数据:如果 plan_node 有2个或以上节点,删除边界节点 """ id: str = Field(..., description="力能ID") plan_start_date: str = Field(..., description="计划首批到场时间") plan_start_quantity: int = Field(..., description="计划首批到场人数") plan_exit_date: DateStr = Field( ..., description="计划退场时间", examples=["2024-06-30"] ) plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表") @field_validator("plan_start_quantity") def validate_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_exit_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_exit_date and cleaned_nodes and cleaned_nodes[-1].date == self.plan_exit_date): cleaned_nodes.pop() self.plan_node = cleaned_nodes return self