SFM/app/pipeline/report.py
2026-07-29 10:28:17 +03:00

205 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)