Files
fa-agent/tests/test_formula_workbook_docs.py
Jayway Liu 9962573379 fix(workbook): F06 参数页改为只读快照,说明页补输入字典与维度对照
- “产品毛利率”参数行不再标记可编辑:说明页写明它是导出时的只读快照,
  各行毛利率已写入计算明细 MarginRate 列,调整需在计算明细页按行修改
- 参数页新增完整维度键-显示名对照行(此前说明页提到“本页分组维度”但无字典)
- 使用说明新增输入字典:Revenue/Cost/GrossInput/MarginRate/ListAmount 逐项列出
  单位、来源字段、修改位置与缺失含义;新增固定结构边界行(哪些列/页不可改、
  改结构需重新导出)
- 测试:参数页全部“否”、每个可修改输入出现在计算明细表头并被汇总公式引用、
  维度对照完整;LibreOffice 用例验证修改 MarginRate 后 lookup 毛利重算(本机跳过)
2026-09-26 01:39:57 +08:00

156 lines
5.5 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.
from io import BytesIO
import pytest
from openpyxl import load_workbook
from finance.exporting import build_formula_workbook
pytestmark = pytest.mark.django_db
EDITABLE_INPUT_HEADERS = ["Revenue", "Cost", "GrossInput", "MarginRate", "ListAmount"]
def _workbook(workspace, **extra):
ds, member, *_ = workspace
output = BytesIO()
build_formula_workbook(
ds,
member,
{"start": "2026-08", "end": "2026-08", "comparison": "yoy", "group": "department", **extra},
output,
)
return load_workbook(BytesIO(output.getvalue()), data_only=False)
def test_parameter_sheet_is_read_only_snapshot(workspace):
workbook = _workbook(workspace)
rows = list(workbook["参数与口径"].iter_rows(values_only=True))
header = list(rows[0])
editable = header.index("可编辑")
assert all(row[editable] == "否" for row in rows[1:] if row[0])
margin = next(row for row in rows if row[0] and "产品毛利率" in str(row[0]))
assert "只读" in margin[2]
assert "MarginRate" in margin[2]
assert "计算明细" in margin[2]
# 维度键与显示名完整对照
texts = [str(row[0]) for row in rows if row[0]]
assert "维度 department" in texts and "维度 product" in texts
department = next(row for row in rows if row[0] == "维度 department")
assert department[1] == "科室"
product = next(row for row in rows if row[0] == "维度 product")
assert product[1] == "产品一级分类"
def test_instructions_document_editable_inputs_with_real_dependencies(workspace):
workbook = _workbook(workspace)
instructions = {
str(row[0]): str(row[1]) for row in workbook["使用说明"].iter_rows(values_only=True) if row[0]
}
data_headers = [cell.value for cell in workbook["计算明细"][1]]
sources = {
"Revenue": "实际收入",
"Cost": "成本",
"GrossInput": "毛利贡献",
"MarginRate": "产品毛利率表",
"ListAmount": "应收金额",
}
for header in EDITABLE_INPUT_HEADERS:
key = next(k for k in instructions if k.startswith(f"输入 {header}("))
text = instructions[key]
assert f"计算明细页 {header} 列" in text
assert sources[header] in text
assert header in data_headers
# 每个可修改输入都被汇总公式真实引用
summary_formula_cells = [
cell.value
for row in workbook["分析汇总"].iter_rows(min_row=6)
for cell in row
if isinstance(cell.value, str) and cell.value.startswith("=IF")
]
joined = "\n".join(summary_formula_cells)
for header in ("Revenue", "Cost", "ListAmount"):
assert f"Data[{header}]" in joined
detail_formula = next(
cell.value
for row in workbook["计算明细"].iter_rows(min_row=2)
for cell in row
if isinstance(cell.value, str) and cell.value.startswith("=IF(")
)
assert "GrossInput" not in detail_formula # 列名按相对引用参与 GrossCalc
assert detail_formula.startswith("=IF(")
boundary = instructions["固定结构"]
assert "人数去重" in boundary and "固定快照" in boundary
assert "重新导出" in boundary
assert "参数与口径" in instructions["维度列"]
def test_margin_rate_lookup_edit_changes_gross_via_detail_recalculation(workspace, tmp_path):
import os
import shutil
from finance.models import Fact
if not (os.environ.get("SOFFICE_BIN") or shutil.which("soffice")):
pytest.skip("set SOFFICE_BIN to run the LibreOffice recalculation check")
ds, member, *_ = workspace
# 去掉直接毛利,使毛利全部走 lookup 毛利率
Fact.objects.filter(dataset=ds).update(gross=None)
mapping = {**ds.mapping, "margin_rates": {"医疗服务": "0.3"}}
ds.mapping = mapping
ds.save(update_fields=["mapping"])
source, output = tmp_path / "source", tmp_path / "out"
source.mkdir()
output.mkdir()
workbook_path = source / "lookup.xlsx"
with workbook_path.open("wb") as file:
build_formula_workbook(
ds,
member,
{"start": "2026-08", "end": "2026-08", "comparison": "yoy", "group": "department"},
file,
)
editable = load_workbook(workbook_path)
# MarginRate 列在维度列之后:G 列(期间+2维度=3 列后第 4 个保留列)
header_row = [cell.value for cell in editable["计算明细"][1]]
rate_column = header_row.index("MarginRate") + 1
editable["计算明细"].cell(row=2, column=rate_column).value = 0.5
editable.save(workbook_path)
import subprocess
profile = tmp_path / "profile"
result = subprocess.run(
[
os.environ.get("SOFFICE_BIN") or shutil.which("soffice"),
"--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)
summary = values["分析汇总"]
gross_row = next(
row
for row in range(6, summary.max_row + 1)
if summary.cell(row, 1).value == "毛利贡献" and summary.cell(row, 2).value == "儿科"
)
# 2026-08 儿科收入 100,lookup 毛利率改为 0.5 后毛利应为 50
assert summary.cell(gross_row, 3).value == pytest.approx(50)