fix(reconcile): F07 对账按指标类型使用独立容差,自定义列改为 Decimal 参考对账

- 对账检查页新增容差列:人数严格相等(容差 0)、金额 0.01、比率 0.0001,
  状态公式改为 ABS(差异)<=容差,不再统一使用 0.01
- 自定义列不再标注“需业务验收/无服务器参考”:新增 to_decimal 白名单表达式
  解释器(仅遍历 metric/number/四则节点,不执行代码)在服务器端用 Decimal
  计算参考值,缺失指标或除零返回 None(对应 #N/A → 无参考)
- 参数页写明对账规则与“容差调整需财务负责人批准”
- 测试:求值器黄金样例(大数/抵消/极小值/除零/缺失/多层表达式)、
  分指标容差列、自定义列 Decimal 参考与缺失场景、全指标覆盖检查;
  LibreOffice 用例验证全部对账行重算后为“通过/无参考”(本机跳过)
This commit is contained in:
Jayway Liu
2026-09-26 01:42:03 +08:00
parent 9962573379
commit 649fec4121
4 changed files with 284 additions and 8 deletions
+40 -5
View File
@@ -21,6 +21,7 @@ from .formula_columns import (
RESERVED_DATA_HEADERS,
ensure_available_metrics,
metrics_used,
to_decimal,
to_excel,
validate_column,
)
@@ -30,6 +31,23 @@ FORMULA_VERSION = "3"
MAX_ROWS = 100_000
MAX_GROUPS = 200
# 对账比较规则:人数严格相等;金额 0.01;比率 0.0001(一个基点)。
# 容差属财务口径,调整需财务负责人批准后方可修改。
RECONCILE_TOLERANCES = {
"customers": Decimal("0"),
"revenue": Decimal("0.01"),
"cost": Decimal("0.01"),
"gross": Decimal("0.01"),
"self_revenue": Decimal("0.01"),
"basket": Decimal("0.0001"),
"margin": Decimal("0.0001"),
"discount_rate": Decimal("0.0001"),
"upgrade_rate": Decimal("0.0001"),
"list_basket": Decimal("0.0001"),
"roi": Decimal("0.0001"),
}
CUSTOM_COLUMN_TOLERANCE = Decimal("0.01")
logger = logging.getLogger(__name__)
@@ -476,6 +494,12 @@ def build_formula_workbook(ds, member, export_context, output):
],
["重算范围", f"最多 {MAX_ROWS:,} 行、{MAX_GROUPS} 组", "超限需缩小导出范围", "否"],
["兼容性", "SUMIFS/COUNTIFS", "移除动态数组人数公式;仍需目标 Excel 桌面版本验收", "否"],
[
"对账容差",
"人数严格相等;金额 0.01;比率 0.0001",
"对账检查页按指标类型使用独立容差;自定义列按金额容差 0.01。容差调整需财务负责人批准",
"否",
],
]:
parameters.append(row)
for d in dims:
@@ -650,11 +674,18 @@ def build_formula_workbook(ds, member, export_context, output):
reconcile.append(
_styled_row(
reconcile,
["指标", "分组", "期间侧", "服务器 Decimal 参考", "公式结果", "差异", "状态"],
["指标", "分组", "期间侧", "服务器 Decimal 参考", "公式结果", "差异", "容差", "状态"],
green,
white,
)
)
def _reconcile_status(row_number):
return (
f'=IF(D{row_number}="","无参考",IF(ISNUMBER(F{row_number}),'
f'IF(ABS(F{row_number})<=G{row_number},"通过","待复核"),"待复核"))'
)
reconcile_row = 2
for label in labels:
for side_name, periods, col in (("本期", ctx["current"], "C"), ("基期", ctx["baseline"], "D")):
@@ -671,21 +702,25 @@ def build_formula_workbook(ds, member, export_context, output):
"" if value is None else str(value),
f"='分析汇总'!{col}{formula_row}",
f'=IFERROR(E{reconcile_row}-D{reconcile_row},"")',
f'=IF(D{reconcile_row}="","无参考",IF(ISNUMBER(F{reconcile_row}),IF(ABS(F{reconcile_row})<0.01,"通过","待复核"),"待复核"))',
float(RECONCILE_TOLERANCES[key]),
_reconcile_status(reconcile_row),
]
)
reconcile_row += 1
for index, definition in enumerate(custom_columns):
formula_row = metric_rows[(label, f"custom_{index}")]
# 自定义列用 Decimal 白名单解释器计算服务器参考,不再依赖业务人工验收
reference_value = to_decimal(definition["expression"], reference)
reconcile.append(
[
_text_cell(reconcile, definition["name"]),
_text_cell(reconcile, label),
side_name,
"自定义公式无服务器参考",
"" if reference_value is None else str(reference_value),
f"='分析汇总'!{col}{formula_row}",
"",
"需业务验收",
f'=IFERROR(E{reconcile_row}-D{reconcile_row},"")',
float(CUSTOM_COLUMN_TOLERANCE),
_reconcile_status(reconcile_row),
]
)
reconcile_row += 1
+25
View File
@@ -1,4 +1,5 @@
import math
from decimal import Decimal
from .analysis import METRICS
@@ -107,3 +108,27 @@ def to_excel(node, references):
if "number" in node:
return str(node["number"])
return f"({to_excel(node['left'], references)}{OPERATORS[node['operator']]}{to_excel(node['right'], references)})"
def to_decimal(node, values):
"""按已校验的白名单表达式树用 Decimal 计算服务器参考值。
只遍历 metric/number/四则运算节点,不执行任何代码;依赖指标缺失或除零时
返回 None,与 Excel 侧 #N/A 的语义一致。
"""
if "metric" in node:
return values[node["metric"]]
if "number" in node:
return Decimal(str(node["number"]))
left, right = to_decimal(node["left"], values), to_decimal(node["right"], values)
if left is None or right is None:
return None
if node["operator"] == "add":
return left + right
if node["operator"] == "subtract":
return left - right
if node["operator"] == "multiply":
return left * right
if right == 0:
return None
return left / right