PayrollManagementSystem/backend/app/services/calc_templates.py

178 lines
5.7 KiB
Python
Raw Normal View History

"""计提公式模板骨架注册与解析。
一期骨架仍是代码内受控集合:页面可维护模板名称、说明、参数绑定(JSON),
但不允许自由表达式,避免注入与算错。数据库定义优先;迁移前的旧库用
`SYSTEM_TEMPLATES` 兜底解析,保证读写与历史行为一致。
"""
from dataclasses import dataclass
from sqlalchemy.orm import Session
@dataclass(frozen=True, slots=True)
class FormulaSpec:
"""公式骨架元数据:参数角色 → 模板参数映射键。"""
code: str
name: str
roles: tuple[str, ...]
description: str
# 骨架类型(formula_type)。新增骨架需同步执行引擎分派点。
FORMULAS: dict[str, FormulaSpec] = {
"linear_decay": FormulaSpec(
code="linear_decay",
name="天数线性递减",
roles=("start", "slope", "floor", "boundary"),
description="按回款天数线性递减,超过截止天数取下限比例",
),
"fixed_profit": FormulaSpec(
code="fixed_profit",
name="折算毛利固定比例",
roles=("rate",),
description="折算毛利润额 × 固定比例",
),
"fixed_sales": FormulaSpec(
code="fixed_sales",
name="销售额固定比例",
roles=("rate",),
description="本人销售额 × 固定比例",
),
"manager_share": FormulaSpec(
code="manager_share",
name="经理团队分成",
roles=("team", "company"),
description="本团队绩效 × 团队比例 + 全公司绩效 × 公司比例",
),
"prepay_fixed": FormulaSpec(
code="prepay_fixed",
name="预付款固定比例",
roles=("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="乘法:预付回款额 × 计提比例",
),
}