PayrollManagementSystem/backend/app/services/calc_template_service.py
liuwenxin b3de35c2ec fix(核算): 对齐抵顶口径并完善界面细节
回款确认时间收敛到秒级;抵顶回款不再自动折算并保留标记。补齐公式模板角色标签与提示、指导成本登记入口及基础布局、分页、设置等响应式细节。同步忽略本地排查产物。
2026-11-06 16:27:45 +08:00

203 lines
7.0 KiB
Python
Raw 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.

"""计提公式模板管理服务。
页面可维护:名称、说明、参数绑定(受骨架角色约束);公式骨架是受控枚举,
创建后锁定,避免自由表达式注入与执行错位。系统模板随迁移种子初始化,
禁删且骨架/编码/参数锁定;自定义模板无岗位引用才可删除。
"""
from sqlalchemy.exc import IntegrityError
from app.core.constants import ACTION_CREATE, ACTION_DELETE, ACTION_UPDATE
from app.core.exceptions import BadRequestError, ForbiddenError, NotFoundError
from app.core.response import PageResult
from app.models import CalcTemplateDefinition, User
from app.repositories import calc_template_repository
from app.schemas.calc_template import (
CalcTemplateCreateIn,
CalcTemplateOut,
CalcTemplateUpdateIn,
)
from app.services import audit_service
from app.services.audit_service import AuditRecord, RequestMeta
from app.services.calc_templates import FORMULAS, get_formula
from app.services.param_registry import PARAM_REGISTRY
MODULE_CALC_TEMPLATE = "公式模板"
def _to_out(row: CalcTemplateDefinition, role_count: int = 0) -> CalcTemplateOut:
return CalcTemplateOut(
id=row.id,
template_code=row.template_code,
template_name=row.template_name,
formula_type=row.formula_type,
params=dict(row.params_json or {}),
description=row.description,
sort_order=row.sort_order,
status=row.status,
is_system=row.is_system,
role_count=role_count,
)
def _validate_params(formula_type: str, params: dict[str, str]) -> dict[str, str]:
"""骨架角色必须全覆盖且不引入多余角色;目标参数必须已注册。"""
spec = get_formula(formula_type)
expected = set(spec.roles)
provided = set(params)
if provided != expected:
missing = sorted(expected - provided)
extra = sorted(provided - expected)
details = []
if missing:
details.append("缺少参数角色:" + "、".join(missing))
if extra:
details.append("多余参数角色:" + "、".join(extra))
raise BadRequestError(";".join(details))
unknown = sorted(code for code in params.values() if code not in PARAM_REGISTRY)
if unknown:
raise BadRequestError("参数编码未注册:" + "、".join(unknown))
return {role: params[role] for role in spec.roles}
def list_templates(session) -> PageResult[CalcTemplateOut]:
"""模板列表(含停用与引用计数,供管理页)。"""
rows = calc_template_repository.list_all(session)
counts = calc_template_repository.count_roles_by_codes(session, [r.template_code for r in rows])
return PageResult[CalcTemplateOut](
records=[_to_out(row, counts.get(row.template_code, 0)) for row in rows],
total=len(rows),
page_num=1,
page_size=max(len(rows), 1),
)
def list_formula_options() -> list[dict]:
"""骨架下拉(受控集合)。"""
return [
{
"code": spec.code,
"name": spec.name,
"description": spec.description,
"roles": list(spec.roles),
"role_labels": dict(spec.role_labels),
"role_hints": dict(spec.role_hints),
}
for spec in FORMULAS.values()
]
def create_template(
session,
payload: CalcTemplateCreateIn,
*,
operator: User,
meta: RequestMeta,
) -> CalcTemplateOut:
"""新增模板。编码唯一;骨架受控;参数绑定按骨架角色校验。"""
if calc_template_repository.get_by_code(session, payload.template_code) is not None:
raise BadRequestError(f"模板编码 {payload.template_code} 已存在")
try:
get_formula(payload.formula_type)
except ValueError as exc:
raise BadRequestError(str(exc)) from exc
params = _validate_params(payload.formula_type, payload.params)
row = CalcTemplateDefinition(
template_code=payload.template_code,
template_name=payload.template_name,
formula_type=payload.formula_type,
params_json=params,
description=payload.description,
sort_order=payload.sort_order,
status=1,
is_system=0,
)
try:
calc_template_repository.create(session, row)
session.commit()
except IntegrityError as exc:
session.rollback()
raise BadRequestError(f"模板编码 {payload.template_code} 已存在") from exc
with audit_service.audit(
session,
operator=operator,
record=AuditRecord(
module=MODULE_CALC_TEMPLATE,
action=ACTION_CREATE,
target=payload.template_code,
summary=f"新增计提公式模板 {payload.template_name}",
detail={"formula_type": payload.formula_type, "params": params},
),
meta=meta,
):
pass
return _to_out(row)
def update_template(
session,
template_id: int,
payload: CalcTemplateUpdateIn,
*,
operator: User,
meta: RequestMeta,
) -> CalcTemplateOut:
"""编辑模板。系统模板参数锁定;自定义模板参数仍按骨架角色校验。"""
row = calc_template_repository.get_by_id(session, template_id, for_update=True)
if row is None:
raise NotFoundError("模板不存在")
params = dict(row.params_json or {})
if row.is_system != 1:
params = _validate_params(row.formula_type, payload.params)
row.template_name = payload.template_name
row.description = payload.description
row.sort_order = payload.sort_order
row.params_json = params
session.commit()
with audit_service.audit(
session,
operator=operator,
record=AuditRecord(
module=MODULE_CALC_TEMPLATE,
action=ACTION_UPDATE,
target=row.template_code,
summary=f"修改计提公式模板 {row.template_name}",
detail={"params": params},
),
meta=meta,
):
pass
return _to_out(row)
def delete_template(
session,
template_id: int,
*,
operator: User,
meta: RequestMeta,
) -> None:
"""删除模板。系统模板禁删;被岗位引用禁删。"""
row = calc_template_repository.get_by_id(session, template_id, for_update=True)
if row is None:
raise NotFoundError("模板不存在")
if row.is_system == 1:
raise ForbiddenError("系统内置模板不可删除")
counts = calc_template_repository.count_roles_by_codes(session, [row.template_code])
if counts.get(row.template_code):
raise BadRequestError("模板已被岗位引用,不能删除;可先调整岗位模板后再删除")
calc_template_repository.delete(session, row)
session.commit()
with audit_service.audit(
session,
operator=operator,
record=AuditRecord(
module=MODULE_CALC_TEMPLATE,
action=ACTION_DELETE,
target=row.template_code,
summary=f"删除计提公式模板 {row.template_name}",
),
meta=meta,
):
pass