from __future__ import annotations from datetime import date, datetime from pathlib import Path from unittest.mock import patch from openpyxl import load_workbook from app.pipeline.column_registry import COLUMNS from app.pipeline.mapping import FIXED_REPORT_COLUMNS from app.pipeline.report import TOP_GROUP_HEADERS, ReportRow, write_report def test_write_report_streaming_preserves_header_and_formats(tmp_path: Path) -> None: destination = tmp_path / "report.xlsx" rows = [ ReportRow( file_name="SKO115FZ_01_123456789_20260616_X00001.xml", record_id="RID-1", operation_index=1, operation_fields={"СумОперации": "100.50", "ДатаСообщения": "2026-06-16"}, participant_fields={}, ) ] write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] headers = [cell.value for cell in sheet[2]] assert headers == list(FIXED_REPORT_COLUMNS) assert sheet.freeze_panes == "A3" row_values = [ sheet.cell(row=3, column=index).value for index in range(1, len(FIXED_REPORT_COLUMNS) + 1) ] assert "SKO115FZ_01_123456789_20260616_X00001.xml" in row_values date_col_idx = FIXED_REPORT_COLUMNS.index("Дата сообщения") + 1 assert isinstance(sheet.cell(row=3, column=date_col_idx).value, datetime) assert sheet.cell(row=3, column=date_col_idx).number_format == "DD.MM.YYYY" sum_col_idx = FIXED_REPORT_COLUMNS.index("Сумма в валюте проведения") + 1 assert sheet.cell(row=3, column=sum_col_idx).number_format == "#,##0.00" def test_write_report_contains_top_group_header_row(tmp_path: Path) -> None: destination = tmp_path / "report.xlsx" write_report([], destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] assert sheet.cell(row=1, column=1).value == "Служебные поля" merged_ranges = {str(rng) for rng in sheet.merged_cells.ranges} expected_ranges = set() for header in TOP_GROUP_HEADERS: if header.start_col > len(FIXED_REPORT_COLUMNS): continue end_col = min(header.end_col, len(FIXED_REPORT_COLUMNS)) if header.start_col == end_col: continue expected_ranges.add( f"{_column_letter(header.start_col)}1:{_column_letter(end_col)}1" ) assert merged_ranges == expected_ranges def test_top_group_headers_follow_registry_report_groups() -> None: actual_ranges = tuple( (header.start_col, header.end_col) for header in TOP_GROUP_HEADERS ) registry_ranges = tuple( (group.start_index, group.end_index) for group in COLUMNS.report_groups() ) assert actual_ranges == registry_ranges def test_write_report_converts_date_string_to_excel_date(tmp_path: Path) -> None: destination = tmp_path / "report.xlsx" rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={"ДатаСообщения": "23/08/2005"}, participant_fields={}, ) ] write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] date_col_idx = FIXED_REPORT_COLUMNS.index("Дата сообщения") + 1 cell = sheet.cell(row=3, column=date_col_idx) assert isinstance(cell.value, datetime) assert cell.value.date() == date(2005, 8, 23) assert cell.number_format == "DD.MM.YYYY" def test_write_report_keeps_invalid_date_as_text_without_date_format( tmp_path: Path, ) -> None: destination = tmp_path / "report.xlsx" rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={"ДатаСообщения": "не дата"}, participant_fields={}, ) ] write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] date_col_idx = FIXED_REPORT_COLUMNS.index("Дата сообщения") + 1 cell = sheet.cell(row=3, column=date_col_idx) assert cell.value == "не дата" assert cell.number_format != "DD.MM.YYYY" def test_write_report_writes_inn_like_values_as_text(tmp_path: Path) -> None: destination = tmp_path / "report.xlsx" rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={}, participant_fields={"ИННЮЛ": 2820000210.0}, ) ] write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] inn_col_idx = next( index for index, name in enumerate(FIXED_REPORT_COLUMNS, start=1) if "ИНН" in name ) cell = sheet.cell(row=3, column=inn_col_idx) assert cell.value == "2820000210" def test_write_report_preserves_account_number_as_text(tmp_path: Path) -> None: destination = tmp_path / "report.xlsx" fixed_row = {index: "" for index in range(1, len(FIXED_REPORT_COLUMNS) + 1)} fixed_row[FIXED_REPORT_COLUMNS.index("Номер счета плательщика") + 1] = ( "40702156767000000008" ) fixed_row[FIXED_REPORT_COLUMNS.index("Номер счета получателя") + 1] = ( 4.0702156767e19 ) rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={}, participant_fields={}, ) ] with patch("app.pipeline.report.build_fixed_row_by_index", return_value=fixed_row): write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] payer_col = FIXED_REPORT_COLUMNS.index("Номер счета плательщика") + 1 receiver_col = FIXED_REPORT_COLUMNS.index("Номер счета получателя") + 1 assert sheet.cell(row=3, column=payer_col).value == "40702156767000000008" assert sheet.cell(row=3, column=receiver_col).value == "40702156767000000000" def test_write_report_restores_leading_zeros_for_bik_inn_kpp(tmp_path: Path) -> None: destination = tmp_path / "report.xlsx" fixed_row = {index: "" for index in range(1, len(FIXED_REPORT_COLUMNS) + 1)} fixed_row[109] = "1" fixed_row[FIXED_REPORT_COLUMNS.index("БИК") + 1] = 41012731 fixed_row[FIXED_REPORT_COLUMNS.index("ИНН") + 1] = 323086051 fixed_row[FIXED_REPORT_COLUMNS.index("КПП (ПРИЗНАК ИСБОЮЛ) /Пр ид-ии ФЛ:") + 1] = ( 32601001 ) rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={}, participant_fields={}, ) ] with patch("app.pipeline.report.build_fixed_row_by_index", return_value=fixed_row): write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] bik_col = FIXED_REPORT_COLUMNS.index("БИК") + 1 inn_col = next( index for index, name in enumerate(FIXED_REPORT_COLUMNS, start=1) if "ИНН" in name ) kpp_col = FIXED_REPORT_COLUMNS.index("КПП (ПРИЗНАК ИСБОЮЛ) /Пр ид-ии ФЛ:") + 1 assert sheet.cell(row=3, column=bik_col).value == "041012731" assert sheet.cell(row=3, column=inn_col).value == "0323086051" assert sheet.cell(row=3, column=kpp_col).value == "032601001" def test_write_report_shifts_participant_address_block_to_expected_columns( tmp_path: Path, ) -> None: destination = tmp_path / "report.xlsx" fixed_row = {index: "" for index in range(1, len(FIXED_REPORT_COLUMNS) + 1)} fixed_row[141] = "г. Благовещенск" fixed_row[142] = "675520, 643, 10, Благовещенский р-н, Чигири с, Зеленая ул, 1" fixed_row[143] = "675520" fixed_row[144] = "643" fixed_row[145] = "10" fixed_row[146] = "Благовещенский р-н" fixed_row[147] = "Чигири с" fixed_row[148] = "Зеленая ул" fixed_row[149] = "1" fixed_row[150] = "" fixed_row[151] = "" rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={}, participant_fields={}, ) ] with patch("app.pipeline.report.build_fixed_row_by_index", return_value=fixed_row): write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] assert sheet.cell(row=3, column=141).value == ( "675520, 643, 10, Благовещенский р-н, Чигири с, Зеленая ул, 1" ) assert sheet.cell(row=3, column=142).value == "675520" assert sheet.cell(row=3, column=143).value == "643" assert sheet.cell(row=3, column=144).value == "10" def test_write_report_does_not_pad_physical_person_identification_as_kpp( tmp_path: Path, ) -> None: destination = tmp_path / "report.xlsx" fixed_row = {index: "" for index in range(1, len(FIXED_REPORT_COLUMNS) + 1)} fixed_row[109] = "2" fixed_row[122] = "1" rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={}, participant_fields={}, ) ] with patch("app.pipeline.report.build_fixed_row_by_index", return_value=fixed_row): write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] assert sheet.cell(row=3, column=122).value == "1" def test_write_report_does_not_assume_kpp_when_participant_type_is_missing( tmp_path: Path, ) -> None: destination = tmp_path / "report.xlsx" fixed_row = {index: "" for index in range(1, len(FIXED_REPORT_COLUMNS) + 1)} fixed_row[122] = "1" rows = [ ReportRow( file_name="f.xml", record_id="RID", operation_index=1, operation_fields={}, participant_fields={}, ) ] with patch("app.pipeline.report.build_fixed_row_by_index", return_value=fixed_row): write_report(rows, destination) workbook = load_workbook(destination) sheet = workbook["Отчет"] assert sheet.cell(row=3, column=122).value == "1" def _column_letter(index: int) -> str: value = index result = "" while value: value, remainder = divmod(value - 1, 26) result = chr(65 + remainder) + result return result