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

220 lines
8.8 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,
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_首日取整10千分_图示口径裁决(self) -> None:
# 需规图示「第 1 天 10‰」2026-10-19 裁决覆盖公式(公式值 9.9889‰ 作废):
# 首日不递减、直接取起始值;第 2 天起才进入公式段
assert _sp_rate(1) == Fraction(10, 1000)
def test_第2天起进入公式递减段(self) -> None:
# 10‰ − 2×4/360‰ = 9.9778‰(首日取整裁决只影响第 1 天,第 2 天起不变)
assert _sp_rate(2) == Fraction(10, 1000) - Fraction(2 * 4, 360 * 1000)
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:
rate = profit_rate(Decimal("100") - Decimal("100"), Decimal("100"))
base = converted_profit(Decimal("100"), rate, Fraction(30))
assert base == Decimal("0")
assert performance_amount(base, Fraction(1, 100)) == 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")