2026-10-12 04:22:32 -04:00
|
|
|
|
"""公式纯函数测试。
|
|
|
|
|
|
|
|
|
|
|
|
守的是:算钱公式的精确性与边界 —— 一旦按 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)
|
|
|
|
|
|
|
2026-10-14 06:58:30 -04:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-10-12 04:22:32 -04:00
|
|
|
|
|
|
|
|
|
|
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
|