diff --git a/api/src/services/export_service/export_writers.py b/api/src/services/export_service/export_writers.py index 196ef63..a6e7fca 100644 --- a/api/src/services/export_service/export_writers.py +++ b/api/src/services/export_service/export_writers.py @@ -1,6 +1,7 @@ from __future__ import annotations import abc +from copy import copy from collections import defaultdict from typing import Any, Callable @@ -255,6 +256,12 @@ class DepthSheetWriter(AbstractWriter): normalized = self.row_normalizer(raw_data) values = [self.value_serializer(normalized.get(key)) for key in self.columns] worksheet.append(values) + for column, key in enumerate(self.columns, start=1): + if key in ("name", "header.name"): + cell = worksheet.cell(row=next_row, column=column) + alignment = copy(cell.alignment) + alignment.indent = outline_level + cell.alignment = alignment self._track_row_widths(column_widths, values) if len(row) > 3: self._set_color_to_row(ws=worksheet, row=next_row, item_type=row[0]) @@ -492,13 +499,14 @@ class XlsxDepthSheetWriter(AbstractWriter): return current_column - def _row_format(self, palette: str, item_type: str): - cache_key = (palette, item_type) + def _row_format(self, palette: str, item_type: str, indent: int = 0): + cache_key = (palette, item_type, indent) if cache_key not in self._format_cache: self._format_cache[cache_key] = self.workbook.add_format( { "bg_color": f"#{PALETTE[palette][item_type]}", "pattern": 1, + "indent": indent, } ) return self._format_cache[cache_key] @@ -531,6 +539,11 @@ class XlsxDepthSheetWriter(AbstractWriter): values = [self.value_serializer(normalized.get(key)) for key in self.columns] if column_formats is not None: worksheet.write_row(output_row, 0, values) + for column, key in enumerate(self.columns): + if key in ("name", "header.name") and outline_level: + properties = dict(self.column_format_resolver(key, column) or {}) + properties["indent"] = outline_level + worksheet.write(output_row, column, values[column], self._resolved_format(properties)) if self.fixed_column_width is None: self._track_row_widths(column_widths, values) output_row += 1 @@ -540,12 +553,14 @@ class XlsxDepthSheetWriter(AbstractWriter): for column, value in enumerate(values): cell_format = None column_key = self.columns[column] + indent = outline_level if column_key in ("name", "header.name") else 0 resolved_format = ( self.row_format_resolver(row, column_key, column) if self.row_format_resolver else None ) if resolved_format: + resolved_format = {**resolved_format, "indent": indent} cell_format = self._resolved_format(resolved_format) palette = self.palette.get(column_key) if ( @@ -553,7 +568,9 @@ class XlsxDepthSheetWriter(AbstractWriter): and palette and item_type not in (None, "SUB_ITEM", "INPUT") ): - cell_format = self._row_format(palette, item_type) + cell_format = self._row_format(palette, item_type, indent) + if cell_format is None and indent: + cell_format = self._resolved_format({"indent": indent}) worksheet.write(output_row, column, value, cell_format) if self.fixed_column_width is None: self._track_row_widths(column_widths, values) diff --git a/api/tests/unit/test_export_writers.py b/api/tests/unit/test_export_writers.py index aa00885..f0339a8 100644 --- a/api/tests/unit/test_export_writers.py +++ b/api/tests/unit/test_export_writers.py @@ -17,20 +17,27 @@ from src.services.export_service.export_writers import ( ) -@pytest.mark.parametrize("backend", ["openpyxl", "xlsxwriter", "xlsxwriter_columns"]) -def test_depth_writer_preserves_collapsible_row_hierarchy(backend): +@pytest.mark.parametrize("backend", ["openpyxl", "xlsxwriter", "xlsxwriter_columns", "xlsxwriter_rows", "xlsxwriter_palette"]) +@pytest.mark.parametrize("name_key", ["name", "header.name"]) +def test_depth_writer_preserves_collapsible_row_hierarchy(backend, name_key): 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} + if backend == "xlsxwriter_rows": + options["row_format_resolver"] = lambda row, key, column: {"bold": True} + if backend == "xlsxwriter_palette": + options["colors_config"] = {"header": {"palette": "green"}} 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"}, + "name": {"name": "Name", "key": name_key}, + "code": {"name": "Code"}, + "contract.name": {"name": "Contract name"}, }}}, row_normalizer=lambda row: row, value_serializer=lambda value: value, @@ -38,7 +45,7 @@ def test_depth_writer_preserves_collapsible_row_hierarchy(backend): ) # 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)]) + writer.write(worksheet, [(kind, 42, index, {name_key: str(index), "code": index, "contract.name": "Contract"}) for index, kind in enumerate(types)]) if backend == "openpyxl": workbook.save(stream) else: @@ -49,6 +56,12 @@ def test_depth_writer_preserves_collapsible_row_hierarchy(backend): 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)) + assert [sheet.cell(index, 1).alignment.indent for index in range(3, 11)] == [0, 1, 2, 2, 1, 2, 3, 0] + assert all(sheet.cell(index, column).alignment.indent == 0 for index in range(3, 11) for column in (2, 3)) + if backend in ("xlsxwriter_columns", "xlsxwriter_rows"): + assert sheet.cell(4, 1).font.bold is True + if backend == "xlsxwriter_palette": + assert sheet.cell(4, 1).fill.fgColor == sheet.cell(4, 2).fill.fgColor def test_tabular_sheet_writer_writes_headers_and_rows():