打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
425 lines
16 KiB
Python
425 lines
16 KiB
Python
"""计提参数目录注册表。
|
||
|
||
注册表是**迁移种子与运行期校验的唯一来源**:编码、默认值、取值域在此定义
|
||
一次;新增参数 = 改这里 + 生成迁移种子(两者一致性由测试锁定)。
|
||
值一律以 Fraction 可解析字符串表示(如 "4/360",禁 0.011111 —— 见算法红线)。
|
||
|
||
`description` 会直接显示在「计提参数」页的参数名下方,**面向业务人员撰写**:
|
||
说清这个参数管什么、钱怎么变、什么时候该调它。不写英文枚举名、需求编号、
|
||
内部阶段号,也不写实现层面的精度约束(那些留在代码注释与测试里)。
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
from fractions import Fraction
|
||
|
||
from app.core.exceptions import BadRequestError
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class ParamSpec:
|
||
"""单个参数的元数据(与 param_definitions 表字段一一对应)。"""
|
||
|
||
code: str
|
||
name: str
|
||
group: str
|
||
value_type: str
|
||
unit: str | None
|
||
dimension_type: str
|
||
options: tuple[str, ...] | None
|
||
default_value: str | None
|
||
value_min: str | None
|
||
value_max: str | None
|
||
description: str | None
|
||
sort_order: int
|
||
|
||
|
||
PARAM_REGISTRY: dict[str, ParamSpec] = {
|
||
spec.code: spec
|
||
for spec in [
|
||
ParamSpec(
|
||
code="salesperson.rate.start",
|
||
name="业务员起始计提比例",
|
||
group="业务员个人户",
|
||
value_type="DECIMAL",
|
||
unit="‰",
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=None,
|
||
default_value="10",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="业务员个人户的起始计提比例;实际比例按回款天数递减,超过分界天数后使用计提下限。",
|
||
sort_order=1,
|
||
),
|
||
ParamSpec(
|
||
code="salesperson.rate.slope",
|
||
name="业务员日递减斜率",
|
||
group="业务员个人户",
|
||
value_type="DECIMAL",
|
||
unit="‰/天",
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=None,
|
||
default_value="4/360",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="回款每延后一天,计提比例减少的幅度。可填写分数,例如 4/360,以保留准确取值。",
|
||
sort_order=2,
|
||
),
|
||
ParamSpec(
|
||
code="salesperson.rate.floor",
|
||
name="业务员计提下限",
|
||
group="业务员个人户",
|
||
value_type="DECIMAL",
|
||
unit="‰",
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=None,
|
||
default_value="4",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="回款天数超过分界天数后,按此固定比例计提。",
|
||
sort_order=3,
|
||
),
|
||
ParamSpec(
|
||
code="salesperson.days.boundary",
|
||
name="线性递减分界天数",
|
||
group="业务员个人户",
|
||
value_type="INT",
|
||
unit="天",
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=None,
|
||
default_value="360",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="按日递减的回款天数上限。超过该天数后,使用业务员计提下限。",
|
||
sort_order=4,
|
||
),
|
||
ParamSpec(
|
||
code="manager.share.team",
|
||
name="经理本团队分成比例",
|
||
group="经理分成",
|
||
value_type="DECIMAL",
|
||
unit="%",
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=None,
|
||
default_value="12.5",
|
||
value_min="0",
|
||
value_max="100",
|
||
description="按分管业务员的绩效总额计算团队分成。正式核算使用发货时登记的业务分管关系。",
|
||
sort_order=5,
|
||
),
|
||
ParamSpec(
|
||
code="manager.share.company",
|
||
name="经理全公司分成比例",
|
||
group="经理分成",
|
||
value_type="DECIMAL",
|
||
unit="%",
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=None,
|
||
default_value="5",
|
||
value_min="0",
|
||
value_max="100",
|
||
description="按全公司业务员的绩效总额计算公司分成,并与本团队分成相加。",
|
||
sort_order=6,
|
||
),
|
||
ParamSpec(
|
||
code="enterprise.rate",
|
||
name="企业户计提比例",
|
||
group="企业户",
|
||
value_type="DECIMAL",
|
||
unit="‰",
|
||
dimension_type="PRODUCT_CATEGORY",
|
||
options=None,
|
||
default_value="3",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="企业户按此比例计提,不随回款天数递减。实际适用值以回款到账日的生效记录为准。",
|
||
sort_order=7,
|
||
),
|
||
ParamSpec(
|
||
code="info.rate",
|
||
name="信息员计提比例",
|
||
group="信息员",
|
||
value_type="DECIMAL",
|
||
unit="‰",
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=None,
|
||
default_value="1",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="信息员按本人销售额和此比例计提,不随回款天数递减。",
|
||
sort_order=8,
|
||
),
|
||
ParamSpec(
|
||
code="prepay.rate",
|
||
name="预付款计提比例",
|
||
group="支付环节",
|
||
value_type="DECIMAL",
|
||
unit="‰",
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="10",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="历史核算参数。正式核算中,预收款在实际发货后才参与计提,并按最高计提比例计算。",
|
||
sort_order=9,
|
||
),
|
||
ParamSpec(
|
||
code="barter.discount",
|
||
name="以物抵顶折算比例",
|
||
group="支付环节",
|
||
value_type="DECIMAL",
|
||
unit="%",
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="80",
|
||
value_min="0",
|
||
value_max="100",
|
||
description="历史核算参数,用于以物抵顶绩效折算。当前特殊抵顶业务按线下流程单独核算。",
|
||
sort_order=10,
|
||
),
|
||
ParamSpec(
|
||
code="payment.start.ratio",
|
||
name="支付起付比例",
|
||
group="支付环节",
|
||
value_type="DECIMAL",
|
||
unit="%",
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="60",
|
||
value_min="0",
|
||
value_max="100",
|
||
description="累计回款达到不含运费销售额的设定比例后,开始计付绩效。启用前应收先冲抵,再核算新业务。",
|
||
sort_order=11,
|
||
),
|
||
ParamSpec(
|
||
code="receipt.confirmation.enabled",
|
||
name="回款人工确认开关",
|
||
group="回款确认",
|
||
value_type="BOOL",
|
||
unit=None,
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="1",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="默认开启:未人工确认的回款不得正式核算。关闭仅跳过人工确认,仍检查凭证等资格;与工资审核开关独立。",
|
||
sort_order=16,
|
||
),
|
||
ParamSpec(
|
||
code="payment.weight.mode",
|
||
name="支付权重应用方式",
|
||
group="支付环节",
|
||
value_type="ENUM",
|
||
unit=None,
|
||
dimension_type="NONE",
|
||
options=("FULL", "BALANCE"),
|
||
default_value="FULL",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="历史核算的支付基数设置。正式核算按累计回款比例的平方计算累计应付,本月只计与历史应付的差额。",
|
||
sort_order=12,
|
||
),
|
||
ParamSpec(
|
||
code="product.threshold",
|
||
name="折算毛利润额阈值",
|
||
group="产品阈值",
|
||
value_type="DECIMAL",
|
||
unit="%",
|
||
dimension_type="PRODUCT_CATEGORY",
|
||
options=None,
|
||
default_value="30",
|
||
value_min="0",
|
||
value_max="100",
|
||
description="折算毛利润额=不含运费的含税销售额×(毛利率-阈值)。低于阈值时结果可为负值,仍按规则计入。",
|
||
sort_order=13,
|
||
),
|
||
ParamSpec(
|
||
code="salary.basic.mode",
|
||
name="月工资基本工资口径",
|
||
group="月工资口径",
|
||
value_type="ENUM",
|
||
unit=None,
|
||
dimension_type="BUSINESS_ROLE",
|
||
options=("INCLUDE", "DEDUCT", "EXCLUDE"),
|
||
default_value="INCLUDE",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="设置基本工资是否与绩效相加、扣除已发基本工资,或不参与月工资计算。",
|
||
sort_order=14,
|
||
),
|
||
ParamSpec(
|
||
code="probation.annual.threshold",
|
||
name="试用期年累计结算阈值",
|
||
group="月工资口径",
|
||
value_type="DECIMAL",
|
||
unit="元",
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="18000",
|
||
value_min="0",
|
||
value_max=None,
|
||
description="试用期绩效正常核算、暂不发放;年内累计超过此金额的部分,在年底一次性结算。",
|
||
sort_order=15,
|
||
),
|
||
ParamSpec(
|
||
code="approval.secondary_allocation.enabled",
|
||
name="二次分配审批开关",
|
||
group="审批开关",
|
||
value_type="BOOL",
|
||
unit=None,
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="1",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="旧版比例分配方案的审核设置,仅用于历史核算流程。当前正式核算按各人发货对应的绩效分配。",
|
||
sort_order=17,
|
||
),
|
||
ParamSpec(
|
||
code="approval.monthly_settlement.enabled",
|
||
name="月度结算审批开关",
|
||
group="审批开关",
|
||
value_type="BOOL",
|
||
unit=None,
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="1",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="启用系统审核后,设置月度结算是否经过财务复核和领导审核。系统审核关闭时,工资生成即完成,不产生审核流程记录。",
|
||
sort_order=18,
|
||
),
|
||
ParamSpec(
|
||
code="approval.adjustment.enabled",
|
||
name="调整单审批开关",
|
||
group="审批开关",
|
||
value_type="BOOL",
|
||
unit=None,
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="0",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="启用系统审核后,设置调整单是否经过财务复核和领导审核。关闭时调整单直接生效,业务操作记录仍保留。",
|
||
sort_order=19,
|
||
),
|
||
ParamSpec(
|
||
code="manager.personal.included",
|
||
name="经理个人户计入聚合基数",
|
||
group="经理分成",
|
||
value_type="BOOL",
|
||
unit=None,
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="0",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="历史核算的经理个人户汇总设置。当前正式核算排除经理本人业务,不受此参数影响。",
|
||
sort_order=20,
|
||
),
|
||
# ===== 预警规则(M4 ⑧ 工作台,澄清 #10「做成参数化规则便于调整」)=====
|
||
# 首批三条中「同步连续失败」依赖 ⑥ T+ 同步,模块未建前不设参数、不产生条目
|
||
ParamSpec(
|
||
code="warning.enabled",
|
||
name="风险预警总开关",
|
||
group="预警规则",
|
||
value_type="BOOL",
|
||
unit=None,
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="1",
|
||
value_min=None,
|
||
value_max=None,
|
||
description="控制首页的业务风险预警,关闭后停止按回款逾期、审批超时等规则扫描。",
|
||
sort_order=21,
|
||
),
|
||
ParamSpec(
|
||
code="warning.receipt.overdue.days",
|
||
name="回款逾期天数阈值",
|
||
group="预警规则",
|
||
value_type="INT",
|
||
unit="天",
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="90",
|
||
value_min="1",
|
||
value_max=None,
|
||
description="发货后超过设定天数仍未收到回款的批次,会显示在首页风险预警中。",
|
||
sort_order=22,
|
||
),
|
||
ParamSpec(
|
||
code="warning.approval.timeout.days",
|
||
name="审批超时天数阈值",
|
||
group="预警规则",
|
||
value_type="INT",
|
||
unit="天",
|
||
dimension_type="NONE",
|
||
options=None,
|
||
default_value="3",
|
||
value_min="1",
|
||
value_max=None,
|
||
description="审核流程开启后,提交超过设定天数仍未处理的单据,会显示在首页风险预警中。",
|
||
sort_order=23,
|
||
),
|
||
]
|
||
}
|
||
|
||
|
||
for _field, _title, _kind, _unit in (
|
||
("first", "个人户首日比例", "DECIMAL", "‰"),
|
||
("start", "个人户线性阶段起始比例", "DECIMAL", "‰"),
|
||
("slope", "个人户线性阶段日斜率", "DECIMAL", "‰/天"),
|
||
("floor", "个人户超期比例", "DECIMAL", "‰"),
|
||
("boundary", "个人户线性阶段截止天数", "INT", "天"),
|
||
):
|
||
_code = f"coefficient.personal.{_field}"
|
||
PARAM_REGISTRY[_code] = ParamSpec(
|
||
code=_code, name=_title, group="产品系数表", value_type=_kind, unit=_unit,
|
||
dimension_type="PRODUCT_CATEGORY", options=None, default_value=None,
|
||
value_min="1" if _kind == "INT" else "0", value_max="3660" if _kind == "INT" else None,
|
||
description="按产品类别维护;未配置时沿用同日期的通用业务员参数。", sort_order=30 + len(PARAM_REGISTRY),
|
||
)
|
||
|
||
|
||
def get_spec(code: str) -> ParamSpec:
|
||
"""取参数元数据。未注册编码属代码缺陷(500),不是用户错误。"""
|
||
spec = PARAM_REGISTRY.get(code)
|
||
if spec is None:
|
||
raise ValueError(f"参数编码未注册:{code}")
|
||
return spec
|
||
|
||
|
||
def parse_fraction(text: str) -> Fraction:
|
||
"""Fraction 精确解析("4/360" → 1/90,不经 float)。非法输入抛 400。"""
|
||
try:
|
||
return Fraction(text.strip())
|
||
except (ValueError, ZeroDivisionError) as exc:
|
||
raise BadRequestError(f"参数值无法解析为数值:{text}") from exc
|
||
|
||
|
||
def validate_and_normalize(spec: ParamSpec, raw: str) -> str:
|
||
"""按元数据校验并规范化参数值,返回可入库字符串。
|
||
|
||
DECIMAL:Fraction 解析 + 取值域校验,分数式原样保留("4/360" 不截断);
|
||
INT:必须整数;BOOL:仅 0/1;ENUM:必须命中 options;TEXT:原样返回。
|
||
"""
|
||
value = raw.strip()
|
||
if spec.value_type in {"DECIMAL", "INT"}:
|
||
fraction = parse_fraction(value)
|
||
if spec.value_type == "INT" and fraction.denominator != 1:
|
||
raise BadRequestError(f"参数 {spec.code} 必须为整数:{raw}")
|
||
if spec.value_min is not None and fraction < parse_fraction(spec.value_min):
|
||
raise BadRequestError(f"参数 {spec.code} 低于取值下限 {spec.value_min}:{raw}")
|
||
if spec.value_max is not None and fraction > parse_fraction(spec.value_max):
|
||
raise BadRequestError(f"参数 {spec.code} 高于取值上限 {spec.value_max}:{raw}")
|
||
return value
|
||
if spec.value_type == "BOOL":
|
||
if value not in {"0", "1"}:
|
||
raise BadRequestError(f"参数 {spec.code} 仅接受 0/1:{raw}")
|
||
return value
|
||
if spec.value_type == "ENUM":
|
||
if not spec.options or value not in spec.options:
|
||
allowed = "/".join(spec.options or ())
|
||
raise BadRequestError(f"参数 {spec.code} 取值须为 {allowed}:{raw}")
|
||
return value
|
||
return value
|