Clear raw gross values for the current-period lookup rows, edit both matching rows instead of a baseline row, and assert recalculated detail and unchanged baseline totals.
175 lines
6.5 KiB
Python
175 lines
6.5 KiB
Python
from decimal import Decimal
|
||
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
|
||
|
||
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
|
||
# 模拟缺失原始直接毛利的导入行,数据库毛利按原毛利率计算。
|
||
gross_index = next(col["index"] for col in ds.mapping["columns"] if col["role"] == "gross")
|
||
for fact in ds.facts.filter(
|
||
period="2026-08", dimensions__department="儿科", dimensions__product="医疗服务"
|
||
):
|
||
fact.raw[gross_index] = ""
|
||
fact.gross = fact.revenue * Decimal("0.3")
|
||
fact.save(update_fields=["raw", "gross"])
|
||
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)
|
||
details = editable["计算明细"]
|
||
columns = {cell.value: cell.column for cell in details[1]}
|
||
lookup_rows = [
|
||
row
|
||
for row in range(2, details.max_row + 1)
|
||
if details.cell(row, columns["Period"]).value == "2026-08"
|
||
and details.cell(row, columns["department"]).value == "儿科"
|
||
and details.cell(row, columns["product"]).value == "医疗服务"
|
||
]
|
||
assert len(lookup_rows) == 2
|
||
for row in lookup_rows:
|
||
assert details.cell(row, columns["GrossInput"]).value is None
|
||
assert details.cell(row, columns["GrossSource"]).value == "lookup"
|
||
assert details.cell(row, columns["MarginRate"]).value == pytest.approx(0.3)
|
||
details.cell(row, columns["MarginRate"]).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)
|
||
details = values["计算明细"]
|
||
assert [details.cell(row, columns["GrossCalc"]).value for row in lookup_rows] == pytest.approx([40, 10])
|
||
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)
|
||
assert summary.cell(gross_row, 4).value == pytest.approx(90)
|