Merge pull request 'export-hierarchy: иерархия expense-item при выгрузке' (#194) from export-hierarchy into test

Reviewed-on: #194
This commit is contained in:
tsygankoviva 2026-10-21 11:46:45 +03:00
commit f14ea0f189
2 changed files with 65 additions and 4 deletions

View File

@ -21,6 +21,22 @@ RowFormatResolver = Callable[[tuple, str, int], dict[str, Any] | None]
ColumnFormatResolver = Callable[[str, int], dict[str, Any] | None]
def _hierarchical_rows(rows: list[tuple]):
"""Use the same row-type hierarchy as the realtime table, skipping META."""
levels = {"ROOT": 0, "GROUP": 1, "ITEM": 2, "SUB_ITEM": 3, "INPUT": 4}
stack: list[int] = []
for row in rows:
item_type = row[0] if row else None
if item_type == "META":
continue
level = levels.get(item_type, 999)
while stack and stack[-1] >= level:
stack.pop()
yield row, len(stack)
if item_type != "INPUT":
stack.append(level)
class AbstractWriter(abc.ABC):
@staticmethod
@ -226,8 +242,10 @@ class DepthSheetWriter(AbstractWriter):
def write(self, worksheet: Worksheet, rows: list[tuple]) -> None:
column_widths: dict[int, int] = {}
self._write_header(worksheet, column_widths=column_widths)
worksheet.sheet_properties.outlinePr.summaryBelow = False
next_row = worksheet.max_row + 1
for row in rows:
for row, outline_level in _hierarchical_rows(rows):
worksheet.row_dimensions[next_row].outlineLevel = min(outline_level, 7)
if len(row) > 3 and isinstance(row[3], dict):
if row[0] == 'META':
continue
@ -489,6 +507,7 @@ class XlsxDepthSheetWriter(AbstractWriter):
column_widths: dict[int, int] = {}
header_rows = self._get_header_max_row(self.columns_dict)
self._write_header(worksheet, column_widths=column_widths)
worksheet.outline_settings(visible=True, symbols_below=False)
output_row = header_rows
column_formats = None
if self.column_format_resolver:
@ -499,7 +518,8 @@ class XlsxDepthSheetWriter(AbstractWriter):
for column, column_key in enumerate(self.columns)
]
for row in rows:
for row, outline_level in _hierarchical_rows(rows):
worksheet.set_row(output_row, None, None, {"level": min(outline_level, 7)})
if len(row) > 3 and isinstance(row[3], dict):
if row[0] == "META":
continue

View File

@ -1,13 +1,54 @@
from unittest.mock import AsyncMock
from io import BytesIO
from openpyxl import Workbook
import pytest
from openpyxl import Workbook, load_workbook
from xlsxwriter import Workbook as XlsxWorkbook
from src.services.export_service import ExportService
from src.services.export_service.export_mappings import (
FORM1_FLAT_COLUMNS,
SMETA_COLUMNS,
)
from src.services.export_service.export_writers import TabularSheetWriter
from src.services.export_service.export_writers import (
DepthSheetWriter,
TabularSheetWriter,
XlsxDepthSheetWriter,
)
@pytest.mark.parametrize("backend", ["openpyxl", "xlsxwriter", "xlsxwriter_columns"])
def test_depth_writer_preserves_collapsible_row_hierarchy(backend):
stream = BytesIO()
workbook = Workbook() if backend == "openpyxl" else XlsxWorkbook(stream)
worksheet = workbook.active if backend == "openpyxl" else workbook.add_worksheet()
options = {}
if backend == "xlsxwriter_columns":
options["column_format_resolver"] = lambda key, column: {"bold": True}
writer_class = DepthSheetWriter if backend == "openpyxl" else XlsxDepthSheetWriter
if backend != "openpyxl":
options["workbook"] = workbook
writer = writer_class(
columns_dict={"header": {"name": "Header", "key": "", "children": {
"name": {"name": "Name"},
}}},
row_normalizer=lambda row: row,
value_serializer=lambda value: value,
**options,
)
# Missing GROUP/SUB_ITEM levels and misleading numeric depths match frontend cases.
types = ["ROOT", "ITEM", "INPUT", "META", "INPUT", "ITEM", "SUB_ITEM", "INPUT", "ROOT"]
writer.write(worksheet, [(kind, 42, index, {"name": str(index)}) for index, kind in enumerate(types)])
if backend == "openpyxl":
workbook.save(stream)
else:
workbook.close()
stream.seek(0)
sheet = load_workbook(stream).active
assert sheet.sheet_properties.outlinePr.summaryBelow is False
assert [sheet.row_dimensions[index].outlineLevel for index in range(3, 11)] == [0, 1, 2, 2, 1, 2, 3, 0]
assert [sheet.cell(index, 1).value for index in range(3, 11)] == ["0", "1", "2", "4", "5", "6", "7", "8"]
assert all(not sheet.row_dimensions[index].hidden for index in range(3, 11))
def test_tabular_sheet_writer_writes_headers_and_rows():