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)