from unittest.mock import AsyncMock from io import BytesIO 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, SMETA_COLUMNS_BY_FORM, ) from src.services.export_service.export_writers import ( DepthSheetWriter, TabularSheetWriter, XlsxDepthSheetWriter, XlsxTabularSheetWriter, ) # Эталонные порядки колонок листа "Смета" экрана каждой формы (AURORA-1903). SMETA_SCREEN_COLUMNS_BY_FORM = { "FORM_1": [ "section_code", "name", "plan_support_q1", "plan_support_q2", "plan_support_q3", "plan_support_q4", "plan_support_year", "plan_development_q1", "plan_development_q2", "plan_development_q3", "plan_development_q4", "plan_development_year", "plan_total_year", "approved_support_q1", "approved_support_q2", "approved_support_q3", "approved_support_q4", "approved_support_year", "approved_development_q1", "approved_development_q2", "approved_development_q3", "approved_development_q4", "approved_development_year", "approved_total_year", "fact_support_q1", "fact_support_q2", "fact_support_q3", "fact_support_q4", "fact_support_year", "fact_development_q1", "fact_development_q2", "fact_development_q3", "fact_development_q4", "fact_development_year", "fact_total_year", "corrected_support_q2", "corrected_support_q3", "corrected_support_q4", "corrected_development_q2", "corrected_development_q3", "corrected_development_q4", ], "FORM_2": [ "section_code", "name", "plan_support_q1", "plan_support_q2", "plan_support_q3", "plan_support_q4", "plan_support_year", "approved_support_q1", "approved_support_q2", "approved_support_q3", "approved_support_q4", "approved_support_year", "fact_support_q1", "fact_support_q2", "fact_support_q3", "fact_support_q4", "fact_support_year", "corrected_support_q2", "corrected_support_q3", "corrected_support_q4", ], "FORM_4": [ "section_code", "name", "plan_development_q1", "plan_development_q2", "plan_development_q3", "plan_development_q4", "plan_development_year", "approved_development_q1", "approved_development_q2", "approved_development_q3", "approved_development_q4", "approved_development_year", "fact_development_q1", "fact_development_q2", "fact_development_q3", "fact_development_q4", "fact_development_year", "corrected_development_q2", "corrected_development_q3", "corrected_development_q4", ], } @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", "key": name_key}, "code": {"name": "Code"}, "contract.name": {"name": "Contract 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_key: str(index), "code": index, "contract.name": "Contract"}) 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)) 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(): writer = TabularSheetWriter( columns=[("a", "A"), ("b", "B")], row_normalizer=lambda row: row, value_serializer=lambda value: value, ) wb = Workbook() ws = wb.active rows = [(None, None, None, {"a": 1, "b": 2})] writer.write(ws, rows) assert ws.cell(row=1, column=1).value == "A" assert ws.cell(row=1, column=2).value == "B" assert ws.cell(row=2, column=1).value == 1 assert ws.cell(row=2, column=2).value == 2 def test_sheet_column_contracts_are_stable(): assert len(FORM1_FLAT_COLUMNS) == 17 assert len(SMETA_COLUMNS) == 41 assert FORM1_FLAT_COLUMNS[0] == ("section", "Код раздела") assert SMETA_COLUMNS[0] == ("section_code", "Код раздела") # AURORA-1903: SMETA_COLUMNS остаётся алиасом на FORM_1-раскладку. assert SMETA_COLUMNS is SMETA_COLUMNS_BY_FORM["FORM_1"] assert [key for key, _ in SMETA_COLUMNS_BY_FORM["FORM_2"]] == SMETA_SCREEN_COLUMNS_BY_FORM["FORM_2"] assert [key for key, _ in SMETA_COLUMNS_BY_FORM["FORM_4"]] == SMETA_SCREEN_COLUMNS_BY_FORM["FORM_4"] @pytest.mark.parametrize("form_type_code", ["FORM_1", "FORM_2", "FORM_4"]) def test_smeta_writer_columns_match_screen_order_per_form(form_type_code): """AURORA-1903: лист "Смета" каждой формы выгружается в порядке своего экрана.""" service = ExportService(AsyncMock()) workbook = XlsxWorkbook(BytesIO()) writers = service._create_xlsxwriter_sheet_writers( workbook=workbook, form_type_code=form_type_code, year=2024, ) smeta_writer = writers["SMETA"] assert [key for key, _ in smeta_writer.columns] == SMETA_SCREEN_COLUMNS_BY_FORM[form_type_code] workbook.close() def test_smeta_writer_falls_back_to_form1_layout_for_unknown_form_type(): service = ExportService(AsyncMock()) workbook = XlsxWorkbook(BytesIO()) writers = service._create_xlsxwriter_sheet_writers( workbook=workbook, form_type_code=None, year=2024, ) assert [key for key, _ in writers["SMETA"].columns] == SMETA_SCREEN_COLUMNS_BY_FORM["FORM_1"] workbook.close() class _FakeXlsxWorksheet: """Фейковый worksheet, фиксирующий ровно то, что было записано.""" def __init__(self): self.written_rows: dict[int, list] = {} def write_row(self, row, col, values): self.written_rows[row] = list(values) def set_column(self, *args, **kwargs): pass def test_xlsx_tabular_sheet_writer_skips_meta_row(): writer = XlsxTabularSheetWriter( workbook=object(), columns=[("section_code", "Код раздела"), ("name", "Наименование"), ("plan_support_q1", "План поддержка I квартал")], row_normalizer=lambda row: row, value_serializer=lambda value: value, ) worksheet = _FakeXlsxWorksheet() rows = [ ("META", -1, 0, {"ignored": True}), ("ROOT", 0, 1, {"section_code": "1", "name": "x", "plan_support_q1": 10}), ] writer.write(worksheet, rows) assert set(worksheet.written_rows.keys()) == {0, 1} assert worksheet.written_rows[0] == ["Код раздела", "Наименование", "План поддержка I квартал"] assert worksheet.written_rows[1] == ["1", "x", 10] def test_tabular_sheet_writer_skips_meta_row_openpyxl(): writer = TabularSheetWriter( columns=[("a", "A"), ("b", "B")], row_normalizer=lambda row: row, value_serializer=lambda value: value, ) wb = Workbook() ws = wb.active rows = [ ("META", -1, 0, {"a": "should-not-appear", "b": "should-not-appear"}), ("ROOT", 0, 1, {"a": 1, "b": 2}), ] writer.write(ws, rows) assert ws.cell(row=1, column=1).value == "A" assert ws.cell(row=1, column=2).value == "B" assert ws.cell(row=2, column=1).value == 1 assert ws.cell(row=2, column=2).value == 2 assert ws.cell(row=3, column=1).value is None def test_export_service_uses_sheet_writer_strategy(): service = ExportService(AsyncMock()) class Recorder: def __init__(self): self.calls = 0 def write(self, worksheet, rows): self.calls += 1 worksheet.append(["ok"]) default_writer = Recorder() smeta_writer = Recorder() service.sheet_writers = {"__default__": default_writer, "SMETA": smeta_writer} wb = Workbook() ws_default = wb.active ws_smeta = wb.create_sheet("smeta") service._write_sheet_rows(ws_default, [], "AHR") service._write_sheet_rows(ws_smeta, [], "SMETA") assert default_writer.calls == 1 assert smeta_writer.calls == 1