PayrollManagementSystem/backend/app/schemas/shipment.py
liuwenxin fba8cfc215 fix(business): 删除保护补齐与单价小数位口径修复(e2e 发现)
- 客户删除保护:被业务引用 → 400(T4 占位注释在业务表就位后漏补,FK 500 暴露)
- 人员删除保护:业务归属(负责/协助)引用纳入检查(同样漏检 500)
- schema 单价类列(DECIMAL 14,4)小数位放行至 4 位:回显 '5000.0000'
  原样提交被两位校验永久拒收(编辑任何带精度单价的历史行都失败)
- 补 2 个集成用例锁定;文案同步「客户归属或业务归属引用」

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 17:06:46 +08:00

139 lines
5.4 KiB
Python
Raw Permalink 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:
"""发货单价列 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="发货日期止(含)")