PayrollManagementSystem/backend/app/schemas/shipment.py

139 lines
5.4 KiB
Python
Raw Normal View History

"""发货管理的请求/响应模型(需求 2.2.2)。
金额/数量入参出参一律 str(服务层 Decimal 运算,全程禁 float),容量与
小数位校验复用 business.py 的 ``_check_amount``(同一套口径,防两处漂移);
字段契约全线蛇形直出(2026-10-13 定稿:无驼峰 alias)。
amount(行金额)与 total_amount(发货总金额)不在入参内 —— 由服务层按
数量×单价 计算固化,不信任前端传值;运费仅记录展示,不计入毛利润口径。
"""
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.core.response import PageQuery
from app.schemas.business import AMOUNT_MAX, PRICE_MAX, _check_amount
class ShipmentItemIn(BaseModel):
"""发货产品明细行(product_id 空=自由录入;价格口径两可,可空人工补)。"""
model_config = ConfigDict(str_strip_whitespace=True)
product_id: int | None = Field(default=None, ge=1, description="产品ID;空=自由录入")
product_name: str = Field(min_length=1, max_length=128, description="产品名称")
spec_model: str | None = Field(
default=None, max_length=128, description="规格型号,空=空串归一"
)
unit: str | None = Field(default=None, max_length=16, description="计量单位")
quantity: str = Field(description="数量,>0")
sale_price: str | None = Field(
default=None, description="发货单价(元),可空=待人工补价,补价前行金额记 0"
)
@field_validator("quantity")
@classmethod
def _check_quantity(cls, value: str) -> str:
return _check_amount(value, "数量", AMOUNT_MAX, allow_positive_only=True) or value
@field_validator("sale_price")
@classmethod
def _check_price(cls, value: str | None) -> str | None:
"""发货单价列 DECIMAL(14,4):按 4 位小数口径(回显可原样提交)。"""
return _check_amount(value, "单价", PRICE_MAX, scale=4)
class ShipmentItemOut(BaseModel):
"""发货产品明细行响应。金额/数量一律 str。"""
model_config = ConfigDict(from_attributes=True)
id: int
product_id: int | None = None
product_name: str
spec_model: str | None = None
unit: str | None = None
quantity: str
sale_price: str | None = None
amount: str
class ShipmentOut(BaseModel):
"""发货单响应(列表与写操作回包共用);business_no 为 join 附带列。"""
model_config = ConfigDict(from_attributes=True)
id: int
shipment_no: str
business_id: int
business_no: str | None = None
ship_date: date
freight_amount: str
total_amount: str
remark: str | None = None
class ShipmentDetailOut(ShipmentOut):
"""发货单详情:附产品明细行。"""
items: list[ShipmentItemOut] = Field(default_factory=list)
class ShipmentCreateIn(BaseModel):
"""创建发货单:业务维度 + 明细行数组(明细 ≥1 行)。
字段名全线蛇形直出(同 business.py 契约,仅分页信封 list/pageNum 驼峰)。
"""
model_config = ConfigDict(str_strip_whitespace=True)
business_id: int = Field(ge=1, description="业务ID")
ship_date: date = Field(description="发货日期(回款天数基准日,发货日首日=第 1 天)")
freight_amount: str | None = Field(
default=None, description="运费金额(元),空记 0;仅记录展示,不计入毛利润口径"
)
items: list[ShipmentItemIn] = Field(min_length=1, description="发货明细行,至少 1 行")
remark: str | None = Field(default=None, max_length=255)
@field_validator("freight_amount")
@classmethod
def _check_freight(cls, value: str | None) -> str | None:
"""运费同样是金额源数据:复用 _check_amount 拒负数/多小数位/超容量。"""
return _check_amount(value, "运费金额", AMOUNT_MAX)
class ShipmentUpdateIn(BaseModel):
"""编辑发货单(明细全量替换)。
business_id 不在编辑入参内 —— 发货单挂靠的业务创建时定死,改挂会让
已指定本发货单的回款语义断裂。
"""
model_config = ConfigDict(str_strip_whitespace=True)
ship_date: date = Field(description="发货日期(回款天数基准日,发货日首日=第 1 天)")
freight_amount: str | None = Field(default=None, description="运费金额(元),空记 0")
items: list[ShipmentItemIn] = Field(
min_length=1, description="发货明细行,至少 1 行(全量替换)"
)
remark: str | None = Field(default=None, max_length=255)
@field_validator("freight_amount")
@classmethod
def _check_freight(cls, value: str | None) -> str | None:
"""运费同样是金额源数据:复用 _check_amount 拒负数/多小数位/超容量。"""
return _check_amount(value, "运费金额", AMOUNT_MAX)
class ShipmentQuery(PageQuery):
"""发货单列表查询条件(字段名蛇形直出,与前端 api 封装同名对应)。
business_id 可空=全量:发货单以业务维度查看为主(一期),但单号可
独立检索,不强制挂业务条件。
"""
business_id: int | None = Field(default=None, ge=1, description="业务筛选,空=全部")
ship_date_from: date | None = Field(default=None, description="发货日期起(含)")
ship_date_to: date | None = Field(default=None, description="发货日期止(含)")