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)