194 lines
6.8 KiB
Python
194 lines
6.8 KiB
Python
"""计提公式模板骨架注册与解析。
|
||
|
||
一期骨架仍是代码内受控集合:页面可维护模板名称、说明、参数绑定(JSON),
|
||
但不允许自由表达式,避免注入与算错。数据库定义优先;迁移前的旧库用
|
||
`SYSTEM_TEMPLATES` 兜底解析,保证读写与历史行为一致。
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class FormulaSpec:
|
||
"""公式骨架元数据:参数角色 → 模板参数映射键。
|
||
|
||
role_labels 把英文角色键翻译成业务可读的中文标签(模板表单展示),
|
||
role_hints 进一步解释该槽位承载的口径(placeholder 提示)。
|
||
"""
|
||
|
||
code: str
|
||
name: str
|
||
roles: tuple[str, ...]
|
||
role_labels: dict[str, str]
|
||
role_hints: dict[str, str]
|
||
description: str
|
||
|
||
|
||
# 骨架类型(formula_type)。新增骨架需同步执行引擎分派点。
|
||
FORMULAS: dict[str, FormulaSpec] = {
|
||
"linear_decay": FormulaSpec(
|
||
code="linear_decay",
|
||
name="天数线性递减",
|
||
roles=("start", "slope", "floor", "boundary"),
|
||
role_labels={"start": "起始比例", "slope": "日递减斜率", "floor": "下限比例", "boundary": "递减截止天数"},
|
||
role_hints={"start": "回款第 1 天适用的计提比例", "slope": "每延后一天递减的幅度", "floor": "超过截止天数后固定使用的比例", "boundary": "按日递减的天数上限"},
|
||
description="按回款天数线性递减,超过截止天数取下限比例",
|
||
),
|
||
"fixed_profit": FormulaSpec(
|
||
code="fixed_profit",
|
||
name="折算毛利固定比例",
|
||
roles=("rate",),
|
||
role_labels={"rate": "计提比例"},
|
||
role_hints={"rate": "折算毛利润额乘以的固定比例"},
|
||
description="折算毛利润额 × 固定比例",
|
||
),
|
||
"fixed_sales": FormulaSpec(
|
||
code="fixed_sales",
|
||
name="销售额固定比例",
|
||
roles=("rate",),
|
||
role_labels={"rate": "计提比例"},
|
||
role_hints={"rate": "本人销售额乘以的固定比例"},
|
||
description="本人销售额 × 固定比例",
|
||
),
|
||
"manager_share": FormulaSpec(
|
||
code="manager_share",
|
||
name="经理团队分成",
|
||
roles=("team", "company"),
|
||
role_labels={"team": "团队分成比例", "company": "全公司分成比例"},
|
||
role_hints={"team": "本团队业务员绩效乘以的比例", "company": "全公司业务员绩效乘以的比例"},
|
||
description="本团队绩效 × 团队比例 + 全公司绩效 × 公司比例",
|
||
),
|
||
"prepay_fixed": FormulaSpec(
|
||
code="prepay_fixed",
|
||
name="预付款固定比例",
|
||
roles=("rate",),
|
||
role_labels={"rate": "计提比例"},
|
||
role_hints={"rate": "预付回款额乘以的固定比例"},
|
||
description="预付回款额 × 固定比例",
|
||
),
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class TemplateDefinition:
|
||
"""模板定义解析结果:数据库行或旧库兜底系统模板。"""
|
||
|
||
template_code: str
|
||
template_name: str
|
||
formula_type: str
|
||
params: dict[str, str]
|
||
description: str | None
|
||
is_system: bool
|
||
status: int
|
||
|
||
|
||
def get_formula(code: str) -> FormulaSpec:
|
||
"""取骨架元数据;未知编码属配置错误,调用方转用户错误。"""
|
||
spec = FORMULAS.get(code)
|
||
if spec is None:
|
||
raise ValueError(f"公式骨架未注册:{code}")
|
||
return spec
|
||
|
||
|
||
def system_definition(code: str) -> TemplateDefinition | None:
|
||
"""旧库兜底:迁移未应用时按内置注册表合成定义,行为与历史一致。"""
|
||
spec = TEMPLATES.get(code)
|
||
if spec is None:
|
||
return None
|
||
return TemplateDefinition(
|
||
template_code=spec.code,
|
||
template_name=spec.name,
|
||
formula_type=spec.formula_type,
|
||
params=dict(spec.params),
|
||
description=spec.description,
|
||
is_system=True,
|
||
status=1,
|
||
)
|
||
|
||
|
||
def resolve_definition(session: Session, code: str) -> TemplateDefinition:
|
||
"""按编码解析模板定义:数据库优先,旧库兜底系统模板。"""
|
||
from app.repositories import calc_template_repository
|
||
|
||
row = calc_template_repository.get_by_code(session, code)
|
||
if row is not None:
|
||
return TemplateDefinition(
|
||
template_code=row.template_code,
|
||
template_name=row.template_name,
|
||
formula_type=row.formula_type,
|
||
params=dict(row.params_json or {}),
|
||
description=row.description,
|
||
is_system=row.is_system == 1,
|
||
status=row.status,
|
||
)
|
||
fallback = system_definition(code)
|
||
if fallback is None:
|
||
raise ValueError(f"计提公式模板未定义:{code}")
|
||
return fallback
|
||
|
||
|
||
def formula_type_of(session: Session, code: str) -> str:
|
||
"""执行分派用:模板 → 公式骨架类型。"""
|
||
return resolve_definition(session, code).formula_type
|
||
|
||
|
||
# 模板参数角色到参数编码的内置映射(与 V28 种子一致;旧库兜底用)。
|
||
_TPL_SALESPERSON = {
|
||
"start": "salesperson.rate.start",
|
||
"slope": "salesperson.rate.slope",
|
||
"floor": "salesperson.rate.floor",
|
||
"boundary": "salesperson.days.boundary",
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class TemplateSpec:
|
||
"""旧库兜底模板元数据(历史编码锚点)。"""
|
||
|
||
code: str
|
||
name: str
|
||
formula_type: str
|
||
params: dict[str, str]
|
||
description: str
|
||
|
||
|
||
TEMPLATES: dict[str, TemplateSpec] = {
|
||
"TPL_SALESPERSON_DECAY": TemplateSpec(
|
||
code="TPL_SALESPERSON_DECAY",
|
||
name="业务员个人户·天数线性递减",
|
||
formula_type="linear_decay",
|
||
params=_TPL_SALESPERSON,
|
||
description="分段:≤分界天数 = 起始 − 天数×斜率;>分界天数 = 下限",
|
||
),
|
||
"TPL_MANAGER_SHARE": TemplateSpec(
|
||
code="TPL_MANAGER_SHARE",
|
||
name="营销经理·团队与全公司分成",
|
||
formula_type="manager_share",
|
||
params={"team": "manager.share.team", "company": "manager.share.company"},
|
||
description="聚合:本团队业务员绩效×团队比例 + 全公司业务员绩效×公司比例",
|
||
),
|
||
"TPL_ENTERPRISE_FIXED": TemplateSpec(
|
||
code="TPL_ENTERPRISE_FIXED",
|
||
name="企业户·固定计提",
|
||
formula_type="fixed_profit",
|
||
params={"rate": "enterprise.rate"},
|
||
description="乘法:折算毛利润额 × 计提比例",
|
||
),
|
||
"TPL_INFO_SALES_FIXED": TemplateSpec(
|
||
code="TPL_INFO_SALES_FIXED",
|
||
name="信息员·销售额固定比例",
|
||
formula_type="fixed_sales",
|
||
params={"rate": "info.rate"},
|
||
description="乘法:本人销售额 × 计提比例",
|
||
),
|
||
"TPL_PREPAY_FIXED": TemplateSpec(
|
||
code="TPL_PREPAY_FIXED",
|
||
name="预付款·固定计提",
|
||
formula_type="prepay_fixed",
|
||
params={"rate": "prepay.rate"},
|
||
description="乘法:预付回款额 × 计提比例",
|
||
),
|
||
}
|