205 lines
7.1 KiB
Python
205 lines
7.1 KiB
Python
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)
|