- V7 四表:performance_ledgers(append-only 版本行,uk(receipt_id,calc_version) 库级防重兜底)/monthly_settlements(月份 uk,通过即锁定终态)/ settlement_items(按人固化可负工资,uk 三列)/adjustment_orders(TZ 单号) - registry +1:approval.adjustment.enabled(19 项,BOOL 首组扩至三开关) - formula 补核算纯函数:clamp_weight_r(从预览内联提取共用)/monthly_salary 三口径/probation_annual_note/manager_team_share(#12 暂口径:基数不含 经理个人户业绩,聚合集中一处答复后只改) - 迁移 downgrade 只保留 drop_table+alter 回滚+种子行反向 DELETE; performance_ledgers 表名复数(test_models 红线断言) - 断言同步:EXPECTED_TABLES 四新表/registry 19/BOOL 三开关/conftest 清理四新表与 ParamValue 边界 22 - schema_sync+全量单测 460 passed Co-Authored-By: Claude Code <noreply@anthropic.com>
209 lines
8.2 KiB
Python
209 lines
8.2 KiB
Python
"""公式纯函数测试。
|
||
|
||
守的是:算钱公式的精确性与边界 —— 一旦按 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,
|
||
clamp_weight_r,
|
||
converted_profit,
|
||
manager_team_share,
|
||
monthly_salary,
|
||
payment_days,
|
||
payment_weight,
|
||
performance_amount,
|
||
probation_annual_note,
|
||
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)
|
||
|
||
def test_负天数钳制为0天_按起始比例计提(self) -> None:
|
||
"""先款后货(payment_days 为负)不线性外插:钳到 0 天取起始比例。
|
||
|
||
修复前 days=-5 会算出 (10 + 5×4/360)‰ —— 高于起始值,多算绩效。
|
||
"""
|
||
assert _sp_rate(-5) == _sp_rate(0)
|
||
assert _sp_rate(-1) == Fraction(10, 1000)
|
||
|
||
|
||
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
|
||
|
||
|
||
class TestClampWeightR:
|
||
"""R 钳制:向下取整 + [0,100] 封顶封底(从预览内联提取的共用实现)。"""
|
||
|
||
def test_向下取整(self) -> None:
|
||
assert clamp_weight_r(Decimal("5999.99"), Decimal("10000")) == 59
|
||
|
||
def test_封顶100_封底0(self) -> None:
|
||
assert clamp_weight_r(Decimal("20000"), Decimal("10000")) == 100
|
||
assert clamp_weight_r(Decimal("0"), Decimal("10000")) == 0
|
||
|
||
def test_销售额0_防御0(self) -> None:
|
||
assert clamp_weight_r(Decimal("100"), Decimal("0")) == 0
|
||
|
||
|
||
class TestMonthlySalary:
|
||
"""月工资分岗口径三档(对照表 #8)。"""
|
||
|
||
def test_INCLUDE_绩效加基本(self) -> None:
|
||
assert monthly_salary(Decimal("1000"), Decimal("3000"), "INCLUDE") == Decimal("4000")
|
||
|
||
def test_DEDUCT_绩效减基本_可为负(self) -> None:
|
||
assert monthly_salary(Decimal("1000"), Decimal("3000"), "DEDUCT") == Decimal("-2000")
|
||
|
||
def test_EXCLUDE_仅绩效(self) -> None:
|
||
assert monthly_salary(Decimal("1000"), Decimal("3000"), "EXCLUDE") == Decimal("1000")
|
||
|
||
def test_基本缺失按0(self) -> None:
|
||
assert monthly_salary(Decimal("1000"), None, "INCLUDE") == Decimal("1000")
|
||
|
||
def test_未知口径报错(self) -> None:
|
||
import pytest
|
||
|
||
with pytest.raises(ValueError):
|
||
monthly_salary(Decimal("1"), Decimal("1"), "UNKNOWN")
|
||
|
||
|
||
class TestProbationAnnualNote:
|
||
"""试用期年累计说明:超阈值才有 note,金额精确到分。"""
|
||
|
||
def test_未超阈值_无说明(self) -> None:
|
||
assert probation_annual_note(Decimal("18000"), Decimal("18000")) is None
|
||
assert probation_annual_note(Decimal("100"), Decimal("18000")) is None
|
||
|
||
def test_超额_给出精确差额(self) -> None:
|
||
note = probation_annual_note(Decimal("20000.5"), Decimal("18000"))
|
||
assert note is not None and "2000.50" in note
|
||
|
||
|
||
class TestManagerTeamShare:
|
||
"""经理 12.5%+5% 聚合(#12 暂口径:基数不含经理个人户业绩——由调用方
|
||
传入的基数集合保证,本函数只做乘法与求和)。"""
|
||
|
||
def test_两段乘加_精确分数(self) -> None:
|
||
# 入参为百分数(与种子一致:12.5 表示 12.5%):10000×12.5%=1250、50000×5%=2500
|
||
got = manager_team_share(
|
||
Decimal("10000"), Decimal("50000"), team_pct=Fraction(25, 2), company_pct=Fraction(5)
|
||
)
|
||
assert got == Decimal("3750")
|
||
|
||
def test_零基数_零结果(self) -> None:
|
||
assert manager_team_share(
|
||
Decimal("0"), Decimal("0"), team_pct=Fraction(0), company_pct=Fraction(0)
|
||
) == Decimal("0")
|