Merge pull request 'export-hierarchy: иерархия expense-item при выгрузке' (#194) from export-hierarchy into test
Reviewed-on: #194
This commit is contained in:
commit
f14ea0f189
@ -21,6 +21,22 @@ RowFormatResolver = Callable[[tuple, str, int], dict[str, Any] | None]
|
|||||||
ColumnFormatResolver = Callable[[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):
|
class AbstractWriter(abc.ABC):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -226,8 +242,10 @@ class DepthSheetWriter(AbstractWriter):
|
|||||||
def write(self, worksheet: Worksheet, rows: list[tuple]) -> None:
|
def write(self, worksheet: Worksheet, rows: list[tuple]) -> None:
|
||||||
column_widths: dict[int, int] = {}
|
column_widths: dict[int, int] = {}
|
||||||
self._write_header(worksheet, column_widths=column_widths)
|
self._write_header(worksheet, column_widths=column_widths)
|
||||||
|
worksheet.sheet_properties.outlinePr.summaryBelow = False
|
||||||
next_row = worksheet.max_row + 1
|
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 len(row) > 3 and isinstance(row[3], dict):
|
||||||
if row[0] == 'META':
|
if row[0] == 'META':
|
||||||
continue
|
continue
|
||||||
@ -489,6 +507,7 @@ class XlsxDepthSheetWriter(AbstractWriter):
|
|||||||
column_widths: dict[int, int] = {}
|
column_widths: dict[int, int] = {}
|
||||||
header_rows = self._get_header_max_row(self.columns_dict)
|
header_rows = self._get_header_max_row(self.columns_dict)
|
||||||
self._write_header(worksheet, column_widths=column_widths)
|
self._write_header(worksheet, column_widths=column_widths)
|
||||||
|
worksheet.outline_settings(visible=True, symbols_below=False)
|
||||||
output_row = header_rows
|
output_row = header_rows
|
||||||
column_formats = None
|
column_formats = None
|
||||||
if self.column_format_resolver:
|
if self.column_format_resolver:
|
||||||
@ -499,7 +518,8 @@ class XlsxDepthSheetWriter(AbstractWriter):
|
|||||||
for column, column_key in enumerate(self.columns)
|
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 len(row) > 3 and isinstance(row[3], dict):
|
||||||
if row[0] == "META":
|
if row[0] == "META":
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -1,13 +1,54 @@
|
|||||||
from unittest.mock import AsyncMock
|
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 import ExportService
|
||||||
from src.services.export_service.export_mappings import (
|
from src.services.export_service.export_mappings import (
|
||||||
FORM1_FLAT_COLUMNS,
|
FORM1_FLAT_COLUMNS,
|
||||||
SMETA_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():
|
def test_tabular_sheet_writer_writes_headers_and_rows():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user