312 lines
10 KiB
Python
312 lines
10 KiB
Python
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
|