PayrollManagementSystem/backend/tests/unit/test_formula.py
liuwenxin 5ed5665a54 feat(system): 计提公式模板注册表与精确分数计算函数
- formula.py 八个纯函数:回款天数(首日计1)、业务员分段递减(360=6‰/361=4‰)、折算毛利润额化简式(防除零)、乘法方向绩效、以物抵顶/R×R/起付
- param_registry.py:16 项参数元数据(与迁移种子同源)+ Fraction 精确解析(4/360 禁截断)+ 类型/取值域/枚举校验
- calc_templates.py:5 个公式模板注册表(岗位绑定的受控集合)
- 新增 43 项单元用例:精确分数断言(899/90000、27 天、9/25),畸形值拒绝

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-12 16:22:32 +08:00

132 lines
5.1 KiB
Python
Raw Permalink 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.

"""公式纯函数测试。
守的是:算钱公式的精确性与边界 —— 一旦按 float 或字面量截断计算,
360/361 天分界、4/360 斜率、27 天回款示例会全部错位,直接影响工资金额。
断言一律用 Fraction/Decimal 精确值,禁近似比较。
"""
from datetime import date
from decimal import Decimal
from fractions import Fraction
from app.services.formula import (
apply_barter,
converted_profit,
payment_days,
payment_weight,
performance_amount,
profit_rate,
salesperson_rate,
start_payment_ok,
)
# 与迁移种子一致的业务员参数(存储口径:start/floor 为 ‰ 数,slope 为 ‰/天)
SP_START = Fraction(10)
SP_SLOPE = Fraction(4, 360)
SP_FLOOR = Fraction(4)
SP_BOUNDARY = 360
def _sp_rate(days: int) -> Fraction:
return salesperson_rate(
days, start=SP_START, slope=SP_SLOPE, floor=SP_FLOOR, boundary_days=SP_BOUNDARY
)
class TestPaymentDays:
"""回款天数:发货日首日计第 1 天(对照表 #1;需规「26 天」示例作废)。"""
def test_发货日当天回款为第1天(self) -> None:
d = date(2026, 1, 1)
assert payment_days(d, d) == 1
def test_12月15日发货1月10日回款为27天(self) -> None:
assert payment_days(date(2025, 12, 15), date(2026, 1, 10)) == 27
def test_跨年按自然日连续累计(self) -> None:
assert payment_days(date(2025, 12, 31), date(2026, 1, 1)) == 2
class TestSalespersonRate:
"""业务员计提比例:1~360 天线性递减、≥361 天固定下限(对照表 #4)。"""
def test_第360天恰为千分之6_线性段终点(self) -> None:
assert _sp_rate(360) == Fraction(6, 1000)
def test_第361天固定下限千分之4_分界线(self) -> None:
# 公式外推为 5.989‰,业务确认「360 为分界线,360 以后按千分之四」
assert _sp_rate(361) == Fraction(4, 1000)
def test_第1天按公式为899比90000_非取整为10(self) -> None:
# 需规图示「第 1 天对应 10‰」为上限参考;公式值 = (10 − 4/360)/1000 = 899/90000
assert _sp_rate(1) == Fraction(899, 90000)
def test_第180天线性段中点千分之8(self) -> None:
assert _sp_rate(180) == Fraction(8, 1000)
def test_返回值为纯比例_非千分数(self) -> None:
# 绩效 = 折算额 × 比例,比例必须是 0.006 而非 6
assert _sp_rate(360) == Fraction(6, 1000)
assert _sp_rate(360) != Fraction(6)
class TestProfitRate:
"""毛利润率:不含运费口径(对照表 #2),毛利润额 / 销售额。"""
def test_毛利润率_不含运费(self) -> None:
assert profit_rate(Decimal("45"), Decimal("100")) == Fraction(9, 20)
def test_销售额为零_返回零_防除零(self) -> None:
assert profit_rate(Decimal("0"), Decimal("0")) == Fraction(0)
class TestConvertedProfit:
"""折算毛利润额(化简式):销售额 ×(毛利润率 − 阈值)(对照表 #12/#13)。"""
def test_化简式结果_市政水利30点阈值(self) -> None:
# 100 × (9/20 − 30/100) = 15
assert converted_profit(Decimal("100"), Fraction(9, 20), Fraction(30)) == Decimal("15")
def test_化简式与定义式等价(self) -> None:
# 定义式:毛利润额 / 毛利润率 × (毛利润率 − 阈值) = 45 / (9/20) × (9/20 − 3/10) = 15
revenue, profit = Decimal("100"), Decimal("45")
rate = profit_rate(profit, revenue)
assert converted_profit(revenue, rate, Fraction(30)) == Decimal("15")
def test_毛利润额为零_折算为零_不除零(self) -> None:
assert converted_profit(Decimal("0"), Fraction(0), Fraction(30)) == Decimal("0")
def test_毛利润率低于阈值_折算为负_不归零(self) -> None:
# 100 × (1/10 − 3/10) = -20(业务确认负值正常计算)
assert converted_profit(Decimal("100"), Fraction(1, 10), Fraction(30)) == Decimal("-20")
class TestPerformanceAmount:
"""绩效 = 折算毛利润额 × 计提比例(乘法方向,对照表 #3)。"""
def test_乘法方向_折算额乘比例(self) -> None:
# 15000 × 97/10000 = 145.5
assert performance_amount(Decimal("15000"), Fraction(97, 10000)) == Decimal("145.5")
def test_负折算额_绩效为负不归零(self) -> None:
assert performance_amount(Decimal("-20"), Fraction(6, 1000)) == Decimal("-0.12")
class TestModifiers:
"""修饰器:以物抵顶、支付权重 R×R、起付判定(对照表 #21/#22/#24)。"""
def test_以物抵顶_按80点折算(self) -> None:
assert apply_barter(Decimal("145.5"), Fraction(80)) == Decimal("116.4")
def test_支付权重_R为60时0点36(self) -> None:
assert payment_weight(60) == Fraction(36, 100)
def test_支付权重_R为100时全额(self) -> None:
assert payment_weight(100) == Fraction(1)
def test_起付判定_达到60点(self) -> None:
assert start_payment_ok(Decimal("60000"), Decimal("100000"), Fraction(60)) is True
def test_起付判定_未达到60点(self) -> None:
assert start_payment_ok(Decimal("59999"), Decimal("100000"), Fraction(60)) is False