Files
fa-agent/tests/test_formula_reconcile_tolerances.py
T
Jayway Liu 649fec4121 fix(reconcile): F07 对账按指标类型使用独立容差,自定义列改为 Decimal 参考对账
- 对账检查页新增容差列:人数严格相等(容差 0)、金额 0.01、比率 0.0001,
  状态公式改为 ABS(差异)<=容差,不再统一使用 0.01
- 自定义列不再标注“需业务验收/无服务器参考”:新增 to_decimal 白名单表达式
  解释器(仅遍历 metric/number/四则节点,不执行代码)在服务器端用 Decimal
  计算参考值,缺失指标或除零返回 None(对应 #N/A → 无参考)
- 参数页写明对账规则与“容差调整需财务负责人批准”
- 测试:求值器黄金样例(大数/抵消/极小值/除零/缺失/多层表达式)、
  分指标容差列、自定义列 Decimal 参考与缺失场景、全指标覆盖检查;
  LibreOffice 用例验证全部对账行重算后为“通过/无参考”(本机跳过)
2026-09-26 01:42:03 +08:00

216 lines
8.1 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.
import os
import shutil
import subprocess
from decimal import Decimal
from io import BytesIO
import pytest
from openpyxl import load_workbook
from finance.exporting import CUSTOM_COLUMN_TOLERANCE, RECONCILE_TOLERANCES, build_formula_workbook
from finance.formula_columns import to_decimal
pytestmark = pytest.mark.django_db
def custom_column(name="收入成本差"):
return {
"name": name,
"meaning": "实际收入减成本",
"scope": "按所选分组及本期、基期计算",
"missing": "结果不可用(#N/A);零分母不可用",
"expression": {
"operator": "subtract",
"left": {"metric": "revenue"},
"right": {"metric": "cost"},
},
}
def test_to_decimal_evaluator_golden_samples():
values = {
"revenue": Decimal("999999999999.99"),
"cost": Decimal("0.0000001"),
"customers": Decimal("7"),
}
# 大数与多层表达式:((收入 - 收入) + 成本×2) ÷ (成本×2) = 1,抵消后不丢失精度
cancellation = {
"operator": "divide",
"left": {
"operator": "add",
"left": {
"operator": "subtract",
"left": {"metric": "revenue"},
"right": {"metric": "revenue"},
},
"right": {"operator": "multiply", "left": {"metric": "cost"}, "right": {"number": 2}},
},
"right": {"operator": "multiply", "left": {"metric": "cost"}, "right": {"number": 2}},
}
assert to_decimal(cancellation, values) == Decimal("1")
# 极小值与比率
assert to_decimal({"metric": "cost"}, values) == Decimal("0.0000001")
assert to_decimal(
{"operator": "divide", "left": {"metric": "revenue"}, "right": {"metric": "customers"}}, values
) == Decimal("999999999999.99") / Decimal("7")
# 除零与缺失指标返回 None(对应 Excel #N/A)
assert to_decimal({"operator": "divide", "left": {"metric": "revenue"}, "right": {"number": 0}}, values) is None
assert to_decimal({"operator": "add", "left": {"metric": "cost"}, "right": {"metric": "customers"}}, {**values, "customers": None}) is None
# 负数与常数组合
assert to_decimal({"operator": "subtract", "left": {"number": -2.5}, "right": {"metric": "cost"}}, values) == Decimal("-2.5") - Decimal("0.0000001")
def test_reconcile_uses_independent_tolerances_per_metric_kind(workspace):
ds, member, *_ = workspace
output = BytesIO()
build_formula_workbook(
ds,
member,
{"start": "2026-08", "end": "2026-08", "comparison": "yoy", "group": "department"},
output,
)
workbook = load_workbook(BytesIO(output.getvalue()), data_only=False)
sheet = workbook["对账检查"]
rows = list(sheet.iter_rows(values_only=True))
assert list(rows[0]) == ["指标", "分组", "期间侧", "服务器 Decimal 参考", "公式结果", "差异", "容差", "状态"]
def find(metric, side="本期"):
return next(row for row in rows[1:] if row[0] == metric and row[2] == side)
assert find("消费人数")[6] == 0 # 人数严格相等
assert find("实际收入")[6] == pytest.approx(0.01)
assert find("毛利贡献")[6] == pytest.approx(0.01)
assert find("毛利率")[6] == pytest.approx(0.0001)
assert find("ROI")[6] == pytest.approx(0.0001)
assert find("客单价")[6] == pytest.approx(0.0001)
# 状态公式按容差列比较(<=G),不再统一 <0.01
status = next(cell.value for cell in sheet[2] if isinstance(cell.value, str) and cell.value.startswith("=IF(D"))
assert "<=G2" in status
# 参数页写明对账规则,容差调整需财务批准
parameters = {str(row[0]): row for row in workbook["参数与口径"].iter_rows(values_only=True) if row[0]}
assert "人数严格相等" in parameters["对账容差"][1]
assert parameters["对账容差"][3] == "否"
def test_custom_columns_reconcile_against_decimal_reference(workspace):
ds, member, *_ = workspace
output = BytesIO()
build_formula_workbook(
ds,
member,
{
"start": "2026-08",
"end": "2026-08",
"comparison": "yoy",
"group": "department",
"custom_columns": [custom_column()],
},
output,
)
workbook = load_workbook(BytesIO(output.getvalue()), data_only=False)
rows = list(workbook["对账检查"].iter_rows(values_only=True))
custom_rows = [row for row in rows[1:] if row[0] == "收入成本差"]
assert custom_rows
pediatrics = next(row for row in custom_rows if row[1] == "儿科" and row[2] == "本期")
assert Decimal(pediatrics[3]) == Decimal("60") # 100 - 40
assert pediatrics[6] == pytest.approx(float(CUSTOM_COLUMN_TOLERANCE))
status = next(
row[7]
for row in workbook["对账检查"].iter_rows(min_row=2, values_only=True)
if row[0] == "收入成本差" and row[1] == "儿科" and row[2] == "本期"
)
assert "<=G" in status and "通过" in status
# 旧的占位文案不再出现
assert all("自定义公式无服务器参考" not in str(row[3]) for row in custom_rows)
assert all(row[7] != "需业务验收" for row in custom_rows)
def test_custom_column_reference_missing_when_dependency_unavailable(workspace):
from finance.models import Fact
ds, member, *_ = workspace
# 外科 2026-08 成本置空 → cost 不可用 → 收入成本差参考为 None → 无参考
Fact.objects.filter(dataset=ds, period="2026-08", dimensions__department="外科").update(cost=None)
output = BytesIO()
build_formula_workbook(
ds,
member,
{
"start": "2026-08",
"end": "2026-08",
"comparison": "yoy",
"group": "department",
"custom_columns": [custom_column()],
},
output,
)
workbook = load_workbook(BytesIO(output.getvalue()), data_only=False)
surgery = next(
row
for row in workbook["对账检查"].iter_rows(min_row=2, values_only=True)
if row[0] == "收入成本差" and row[1] == "外科" and row[2] == "本期"
)
assert surgery[3] in ("", None) # 无服务器参考
assert "无参考" in surgery[7]
def test_reconcile_tolerances_cover_every_metric():
from finance.analysis import METRICS
assert set(RECONCILE_TOLERANCES) == set(METRICS)
assert RECONCILE_TOLERANCES["customers"] == 0
assert all(
tolerance in (Decimal("0.01"), Decimal("0.0001"))
for key, tolerance in RECONCILE_TOLERANCES.items()
if key != "customers"
)
def test_reconcile_passes_in_libreoffice_with_per_metric_tolerances(workspace, tmp_path):
executable = os.environ.get("SOFFICE_BIN") or shutil.which("soffice")
if not executable:
pytest.skip("set SOFFICE_BIN to run the LibreOffice recalculation check")
ds, member, *_ = workspace
source, output = tmp_path / "source", tmp_path / "out"
source.mkdir()
output.mkdir()
workbook_path = source / "reconcile.xlsx"
with workbook_path.open("wb") as file:
build_formula_workbook(
ds,
member,
{
"start": "2026-08",
"end": "2026-08",
"comparison": "yoy",
"group": "department",
"custom_columns": [custom_column()],
},
file,
)
profile = tmp_path / "profile"
result = subprocess.run(
[
executable,
"--headless",
f"-env:UserInstallation={profile.as_uri()}",
"--convert-to",
"xlsx",
"--outdir",
str(output),
str(workbook_path),
],
capture_output=True,
text=True,
timeout=120,
check=False,
)
assert result.returncode == 0, result.stderr or result.stdout
values = load_workbook(output / workbook_path.name, data_only=True)
statuses = [
(row[0], row[1], row[2], row[7])
for row in values["对账检查"].iter_rows(min_row=2, values_only=True)
]
assert statuses
assert all(status in ("通过", "无参考") for *_head, status in statuses), statuses