2026-10-12 08:24:17 -04:00
|
|
|
|
"""计提参数管理的请求/响应模型。
|
|
|
|
|
|
|
|
|
|
|
|
字段命名与 user/role 一致采用蛇形直出(前端 TS 接口同构)。
|
|
|
|
|
|
金额类入参用字符串承载(Decimal 解析),杜绝 float 精度损失。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import date, datetime
|
|
|
|
|
|
from decimal import Decimal, InvalidOperation
|
|
|
|
|
|
|
|
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.response import PageQuery
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_amount(value: str, *, field_name: str, allow_negative: bool) -> str:
|
|
|
|
|
|
"""金额字符串校验:必须可被 Decimal 解析;默认拒绝负数(毛利润额除外)。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
amount = Decimal(value.strip())
|
|
|
|
|
|
except InvalidOperation as exc:
|
|
|
|
|
|
raise ValueError(f"{field_name}必须是有效数字:{value}") from exc
|
2026-10-12 14:28:20 -04:00
|
|
|
|
if not amount.is_finite():
|
|
|
|
|
|
# 'NaN' 的比较会抛 InvalidOperation、'Infinity' 会在后续计算抛溢出,均须在入口拦下
|
|
|
|
|
|
raise ValueError(f"{field_name}必须为有限数字:{value}")
|
2026-10-12 08:24:17 -04:00
|
|
|
|
if not allow_negative and amount < 0:
|
|
|
|
|
|
raise ValueError(f"{field_name}不得为负数:{value}")
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamDimensionValueOut(BaseModel):
|
|
|
|
|
|
"""某参数单个维度的当前生效值。"""
|
|
|
|
|
|
|
|
|
|
|
|
dimension_value: str
|
|
|
|
|
|
value_text: str
|
|
|
|
|
|
effective_date: date | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamCatalogItemOut(BaseModel):
|
|
|
|
|
|
"""参数目录项:定义元数据 + 各维度截至查询时点的生效值。"""
|
|
|
|
|
|
|
|
|
|
|
|
param_code: str
|
|
|
|
|
|
param_name: str
|
|
|
|
|
|
param_group: str
|
|
|
|
|
|
value_type: str
|
|
|
|
|
|
unit: str | None = None
|
|
|
|
|
|
dimension_type: str
|
|
|
|
|
|
options: list[str] = Field(default_factory=list)
|
|
|
|
|
|
default_value: str | None = None
|
|
|
|
|
|
value_min: str | None = None
|
|
|
|
|
|
value_max: str | None = None
|
|
|
|
|
|
description: str | None = None
|
|
|
|
|
|
values: list[ParamDimensionValueOut] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamHistoryOut(BaseModel):
|
|
|
|
|
|
"""单参数的版本历史条目(append-only 留痕)。"""
|
|
|
|
|
|
|
|
|
|
|
|
version_no: str
|
|
|
|
|
|
dimension_value: str
|
|
|
|
|
|
old_value_text: str | None = None
|
|
|
|
|
|
value_text: str
|
|
|
|
|
|
effective_date: date
|
|
|
|
|
|
operator_name: str
|
|
|
|
|
|
create_time: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamChangeItemIn(BaseModel):
|
|
|
|
|
|
"""变更条目:参数 + 维度 + 新值。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(str_strip_whitespace=True)
|
|
|
|
|
|
|
|
|
|
|
|
param_code: str = Field(min_length=1, max_length=64)
|
|
|
|
|
|
dimension_value: str = Field(default="", max_length=32)
|
|
|
|
|
|
value: str = Field(min_length=1, max_length=64)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamChangeIn(BaseModel):
|
|
|
|
|
|
"""提交变更批次。
|
|
|
|
|
|
|
|
|
|
|
|
change_type=1 立即生效(生效日期取今日);change_type=2 指定日期生效,
|
|
|
|
|
|
生效日期必填且不得早于今日(过去没有「预约」的意义,历史回溯走重算授权)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(str_strip_whitespace=True)
|
|
|
|
|
|
|
|
|
|
|
|
change_type: int = Field(ge=1, le=2, description="生效方式:1立即生效 2指定日期生效")
|
|
|
|
|
|
effective_date: date | None = Field(default=None, description="指定日期生效时必填")
|
|
|
|
|
|
remark: str | None = Field(default=None, max_length=255)
|
|
|
|
|
|
items: list[ParamChangeItemIn] = Field(min_length=1, max_length=50)
|
|
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
|
def _check_effective_date(self) -> "ParamChangeIn":
|
|
|
|
|
|
if self.change_type == 2 and self.effective_date is None:
|
|
|
|
|
|
raise ValueError("指定日期生效时必须提供生效日期")
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamChangeBatchOut(BaseModel):
|
|
|
|
|
|
"""变更批次(提交/撤回/列表共用)。"""
|
|
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
|
version_no: str
|
|
|
|
|
|
effective_date: date
|
|
|
|
|
|
change_type: int
|
|
|
|
|
|
remark: str | None = None
|
|
|
|
|
|
operator_name: str
|
|
|
|
|
|
item_count: int
|
|
|
|
|
|
create_time: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamBatchItemOut(BaseModel):
|
|
|
|
|
|
"""批次明细条目(旧值 → 新值)。"""
|
|
|
|
|
|
|
|
|
|
|
|
param_code: str
|
|
|
|
|
|
param_name: str
|
|
|
|
|
|
dimension_value: str
|
|
|
|
|
|
old_value_text: str | None = None
|
|
|
|
|
|
value_text: str
|
|
|
|
|
|
effective_date: date
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamBatchOut(BaseModel):
|
|
|
|
|
|
"""变更批次(列表项)。"""
|
|
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
|
version_no: str
|
|
|
|
|
|
effective_date: date
|
|
|
|
|
|
change_type: int
|
|
|
|
|
|
remark: str | None = None
|
|
|
|
|
|
operator_name: str
|
|
|
|
|
|
item_count: int
|
|
|
|
|
|
create_time: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamBatchDetailOut(BaseModel):
|
|
|
|
|
|
"""批次明细:批次头 + 逐项变更。"""
|
|
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
|
version_no: str
|
|
|
|
|
|
effective_date: date
|
|
|
|
|
|
change_type: int
|
|
|
|
|
|
remark: str | None = None
|
|
|
|
|
|
operator_name: str
|
|
|
|
|
|
item_count: int
|
|
|
|
|
|
create_time: datetime
|
|
|
|
|
|
items: list[ParamBatchItemOut] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamDiffOut(BaseModel):
|
|
|
|
|
|
"""两日期快照对比项(全量参数,changed 标记是否变化)。"""
|
|
|
|
|
|
|
|
|
|
|
|
param_code: str
|
|
|
|
|
|
param_name: str
|
|
|
|
|
|
param_group: str
|
|
|
|
|
|
dimension_value: str
|
|
|
|
|
|
old_value: str | None = None
|
|
|
|
|
|
new_value: str | None = None
|
|
|
|
|
|
changed: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamPreviewIn(BaseModel):
|
|
|
|
|
|
"""计算预览入参。金额用字符串承载,Decimal 解析。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(str_strip_whitespace=True)
|
|
|
|
|
|
|
|
|
|
|
|
business_role: str | None = Field(
|
|
|
|
|
|
default=None, max_length=32, description="岗位编码,如 salesperson"
|
|
|
|
|
|
)
|
|
|
|
|
|
customer_type: str | None = Field(
|
|
|
|
|
|
default=None, max_length=32, description="客户类型:personal个人户 enterprise企业户"
|
|
|
|
|
|
)
|
|
|
|
|
|
product_category: str | None = Field(
|
|
|
|
|
|
default=None, max_length=32, description="产品类别:municipal_water/rail_transit"
|
|
|
|
|
|
)
|
|
|
|
|
|
ship_date: date = Field(description="发货日")
|
|
|
|
|
|
receipt_date: date = Field(description="回款到账日")
|
|
|
|
|
|
revenue: str = Field(min_length=1, description="销售额(不含运费)")
|
|
|
|
|
|
profit: str = Field(min_length=1, description="毛利润额(可为负)")
|
|
|
|
|
|
received_amount: str = Field(min_length=1, description="本笔回款金额")
|
|
|
|
|
|
barter: bool = Field(default=False, description="是否以物抵顶")
|
|
|
|
|
|
probation: bool = Field(default=False, description="是否试用期")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("revenue", "received_amount")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _check_non_negative(cls, value: str, info) -> str:
|
|
|
|
|
|
return _parse_amount(value, field_name=info.field_name, allow_negative=False)
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("profit")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _check_profit(cls, value: str, info) -> str:
|
|
|
|
|
|
return _parse_amount(value, field_name=info.field_name, allow_negative=True)
|
|
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
|
def _check_dates(self) -> "ParamPreviewIn":
|
|
|
|
|
|
if self.receipt_date < self.ship_date:
|
|
|
|
|
|
raise ValueError("回款到账日不得早于发货日")
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamUsedOut(BaseModel):
|
|
|
|
|
|
"""预览实际用到的参数(对账透明:取值与生效时点)。"""
|
|
|
|
|
|
|
|
|
|
|
|
param_code: str
|
|
|
|
|
|
value_text: str
|
|
|
|
|
|
effective_date: date | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamPreviewOut(BaseModel):
|
|
|
|
|
|
"""计算预览出参。分数串(如 97/10000)为精确值,金额为 Decimal 串。"""
|
|
|
|
|
|
|
|
|
|
|
|
payment_days: int
|
|
|
|
|
|
rate: str
|
|
|
|
|
|
rate_text: str
|
|
|
|
|
|
profit_rate: str
|
|
|
|
|
|
converted_profit: str
|
|
|
|
|
|
performance_amount: str
|
|
|
|
|
|
barter_applied: bool
|
|
|
|
|
|
performance_after_barter: str | None = None
|
|
|
|
|
|
start_payment_ok: bool
|
|
|
|
|
|
payment_weight_r: int
|
|
|
|
|
|
payment_weight: str
|
|
|
|
|
|
payable_amount: str
|
|
|
|
|
|
probation_note: str | None = None
|
|
|
|
|
|
params_used: list[ParamUsedOut] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamBatchQuery(PageQuery):
|
|
|
|
|
|
"""变更记录查询条件。"""
|
|
|
|
|
|
|
|
|
|
|
|
start_date: date | None = Field(default=None, description="生效日期起(含)")
|
|
|
|
|
|
end_date: date | None = Field(default=None, description="生效日期止(含)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ParamHistoryQuery(PageQuery):
|
|
|
|
|
|
"""参数历史查询条件。"""
|
|
|
|
|
|
|
|
|
|
|
|
dimension_value: str | None = Field(
|
|
|
|
|
|
default=None, max_length=32, description="按维度值筛选,空为全部"
|
|
|
|
|
|
)
|