from __future__ import annotations import shutil from dataclasses import dataclass from pathlib import Path from tempfile import NamedTemporaryFile from typing import Iterable import xlsxwriter from .mapping import FIXED_REPORT_COLUMNS, build_fixed_row @dataclass class ReportRow: file_name: str record_id: str operation_index: int operation_fields: dict[str, str] participant_fields: dict[str, str] @dataclass(frozen=True) class GroupHeader: start_col: int end_col: int title: str # Grouped upper header from the template report for readability. TOP_GROUP_HEADERS: tuple[GroupHeader, ...] = ( GroupHeader( start_col=1, end_col=4, title="Информация о КО, передающей сведения в уполномоченный орган", ), GroupHeader(start_col=5, end_col=8, title="Сведения об уполномоченном лице"), GroupHeader( start_col=11, end_col=13, title="Информация о филиале КО, передающем сведения в уполномоченный орган", ), GroupHeader(start_col=14, end_col=36, title="Информация об операции"), GroupHeader( start_col=37, end_col=48, title="Сведения о переводах денежных средств, в том числе электронных денежных средств", ), GroupHeader( start_col=49, end_col=53, title="Сведения о месте приема наличных денежных средств", ), GroupHeader( start_col=55, end_col=59, title="Сведения о месте выдачи наличных денежных средств", ), GroupHeader( start_col=60, end_col=64, title="Сведения о месте авторизации ЭСП", ), GroupHeader( start_col=67, end_col=69, title=( "Сведения о внесении наличных денежных средств на свой банковский счет " "или о получении наличных денежных средств со своего банковского счета " "у одного оператора по переводу денежных средств" ), ), GroupHeader( start_col=70, end_col=74, title="Сведения о месте приема (выдачи) наличных денежных средств", ), GroupHeader( start_col=76, end_col=80, title="Сведения о месте совершения операции с денежными средствами в наличной форме", ), GroupHeader(start_col=87, end_col=104, title="Сведения об участниках операции"), GroupHeader(start_col=105, end_col=118, title="Сведения ЕИО"), ) class StreamingReportWriter: def __init__(self) -> None: self.columns = list(FIXED_REPORT_COLUMNS) temp_file = NamedTemporaryFile(suffix=".xlsx", delete=False) self._temp_report_path = Path(temp_file.name) temp_file.close() self.workbook = xlsxwriter.Workbook( str(self._temp_report_path), {"constant_memory": True}, ) self._is_closed = False self.sheet = self.workbook.add_worksheet("Отчет") self._header_format = self.workbook.add_format( { "bold": True, "align": "center", "valign": "vcenter", "text_wrap": True, "bg_color": "#E2F0D9", } ) self._group_format = self.workbook.add_format( { "bold": True, "align": "center", "valign": "vcenter", "text_wrap": True, "bg_color": "#FFFFFF", } ) self._date_format = self.workbook.add_format({"num_format": "DD.MM.YYYY"}) self._sum_format = self.workbook.add_format({"num_format": "#,##0.00"}) self._column_formats = [self._resolve_format(column) for column in self.columns] self._build_group_header_row() self._build_header_row() # Freeze both header rows. self.sheet.freeze_panes(2, 0) self.sheet.autofilter(1, 0, 1, len(self.columns) - 1) self._next_data_row = 2 def append_row(self, row: ReportRow) -> None: data = build_fixed_row( file_name=row.file_name, record_id=row.record_id, operation_index=row.operation_index, operation_fields=row.operation_fields, participant_fields=row.participant_fields, ) values: list[object] = [] for index, column_name in enumerate(self.columns): raw_value = data.get(column_name, "") values.append(raw_value) for index, value in enumerate(values): cell_format = self._column_formats[index] if cell_format is not None: self.sheet.write(self._next_data_row, index, value, cell_format) else: self.sheet.write(self._next_data_row, index, value) self._next_data_row += 1 def save(self, destination: Path) -> None: destination.parent.mkdir(parents=True, exist_ok=True) try: if not self._is_closed: self.workbook.close() self._is_closed = True shutil.copyfile(self._temp_report_path, destination) finally: self._cleanup_temp_file() def _build_group_header_row(self) -> None: for header in TOP_GROUP_HEADERS: if header.start_col > len(self.columns): continue first_col = header.start_col - 1 last_col = min(header.end_col, len(self.columns)) - 1 self.sheet.merge_range( 0, first_col, 0, last_col, header.title, self._group_format, ) def _build_header_row(self) -> None: for index, name in enumerate(self.columns): self.sheet.write(1, index, name, self._header_format) def _resolve_format(self, column_name: str): if "Дата" in column_name: return self._date_format if "Сумм" in column_name or "Сумма" in column_name: return self._sum_format return None def _cleanup_temp_file(self) -> None: try: self._temp_report_path.unlink(missing_ok=True) except Exception: # noqa: BLE001 pass def __del__(self) -> None: if not getattr(self, "_is_closed", True): try: self.workbook.close() except Exception: # noqa: BLE001 pass self._is_closed = True self._cleanup_temp_file() def write_report(rows: Iterable[ReportRow], destination: Path) -> None: writer = StreamingReportWriter() for row in rows: writer.append_row(row) writer.save(destination)