156 lines
6.7 KiB
Python
156 lines
6.7 KiB
Python
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, trim_plan_nodes_by_boundary_dates
|
|
|
|
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"]
|
|
)
|
|
|
|
@model_validator(mode='after')
|
|
def clean_response_plan_nodes(self):
|
|
self.plan_node = trim_plan_nodes_by_boundary_dates(
|
|
self.plan_node,
|
|
start_date=self.plan_start_date,
|
|
end_date=self.plan_complete_date,
|
|
)
|
|
return self
|
|
|
|
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 |