PayrollManagementSystem/backend/app/schemas/shipment.py
liuwenxin 4aea2be103 feat(business): 发货管理接口(发货单/明细/运费/汇总回写/删除保护)
- FOR UPDATE 锁业务行防发货/回款并发丢失更新;终态业务禁新发货
- 行金额服务层按数量×单价固化;明细行 remark 按设计定稿移除(冒烟修复)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 12:25:33 +08:00

126 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""发货管理的请求/响应模型(需求 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:
return _check_amount(value, "金额", PRICE_MAX)
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)
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)
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="发货日期止(含)")