75-123 rule

This commit is contained in:
Raykov-MS 2026-07-30 13:23:44 +03:00
parent 25f96438b1
commit 55f0312929
4 changed files with 179 additions and 26 deletions

View File

@ -106,6 +106,9 @@ def _build_rules(mapping_data: list[dict[str, str | int]]) -> tuple[MappingRule,
candidates = ["ИННФЛИП", "ИННФЛ", "ИННЮЛ", "ИНН", *candidates] candidates = ["ИННФЛИП", "ИННФЛ", "ИННЮЛ", "ИНН", *candidates]
if column == "Тип участника": if column == "Тип участника":
candidates = ["Тип", "ТипУчастника", *candidates] candidates = ["Тип", "ТипУчастника", *candidates]
if column == "Отчество":
# В некоторых выгрузках встречается опечатка тега "Очт".
candidates = ["Отч", "Очт", *candidates]
if path: if path:
for path_tag in _extract_path_candidate_keys(path): for path_tag in _extract_path_candidate_keys(path):
if path_tag not in candidates: if path_tag not in candidates:
@ -334,6 +337,7 @@ def build_fixed_row(
) )
_apply_structured_group_rules(row) _apply_structured_group_rules(row)
_apply_conditional_rules_1_74(row) _apply_conditional_rules_1_74(row)
_apply_conditional_rules_75_123(row)
for column in FIXED_REPORT_COLUMNS: for column in FIXED_REPORT_COLUMNS:
row.setdefault(column, "") row.setdefault(column, "")
return row return row
@ -531,3 +535,19 @@ def _apply_conditional_rules_1_74(row: dict[str, str]) -> None:
# 62. Статус перевода заполняется только для видов 2/5/8. # 62. Статус перевода заполняется только для видов 2/5/8.
if transfer_type not in {"2", "5", "8"}: if transfer_type not in {"2", "5", "8"}:
_set_row_value(row, 62, "") _set_row_value(row, 62, "")
def _apply_conditional_rules_75_123(row: dict[str, str]) -> None:
codes = _operation_and_extra_codes(row)
# 96. Код территории заполняется только для признака 5016 (дубль 36).
if "5016" not in codes:
_set_row_value(row, 96, "")
# 103. Если операция без участия сотрудника и данных нет -> подставляем заглушку.
if _row_value(row, 104) == "0" and not _row_value(row, 103):
_set_row_value(row, 103, "Информация отсутствует")
# 105. Аналогично для наименования иностранного банка.
if _row_value(row, 104) == "0" and not _row_value(row, 105):
_set_row_value(row, 105, "Информация отсутствует")

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import shutil import shutil
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, datetime
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import Iterable from typing import Iterable
@ -135,13 +136,19 @@ class StreamingReportWriter:
operation_fields=row.operation_fields, operation_fields=row.operation_fields,
participant_fields=row.participant_fields, participant_fields=row.participant_fields,
) )
values: list[object] = []
for index, column_name in enumerate(self.columns): for index, column_name in enumerate(self.columns):
raw_value = data.get(column_name, "") raw_value = data.get(column_name, "")
values.append(raw_value) default_format = self._column_formats[index]
for index, value in enumerate(values): value, cell_format = self._normalize_cell_value(
cell_format = self._column_formats[index] column_name=column_name,
if cell_format is not None: raw_value=raw_value,
default_format=default_format,
)
if isinstance(value, datetime) and cell_format is self._date_format:
self.sheet.write_datetime(
self._next_data_row, index, value, cell_format
)
elif cell_format is not None:
self.sheet.write(self._next_data_row, index, value, cell_format) self.sheet.write(self._next_data_row, index, value, cell_format)
else: else:
self.sheet.write(self._next_data_row, index, value) self.sheet.write(self._next_data_row, index, value)
@ -186,6 +193,40 @@ class StreamingReportWriter:
return self._sum_format return self._sum_format
return None return None
def _normalize_cell_value(
self,
*,
column_name: str,
raw_value: object,
default_format,
) -> tuple[object, object | None]:
if default_format is self._date_format:
parsed_date = self._parse_date_value(raw_value)
if parsed_date is not None:
return parsed_date, self._date_format
if raw_value in (None, ""):
return "", None
return raw_value, None
return raw_value, default_format
def _parse_date_value(self, value: object) -> datetime | None:
if isinstance(value, datetime):
return value
if isinstance(value, date):
return datetime.combine(value, datetime.min.time())
if not isinstance(value, str):
return None
normalized = value.strip()
if not normalized:
return None
date_part = normalized.split()[0]
for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%d.%m.%Y", "%Y/%m/%d"):
try:
return datetime.strptime(date_part, fmt)
except ValueError:
continue
return None
def _cleanup_temp_file(self) -> None: def _cleanup_temp_file(self) -> None:
try: try:
self._temp_report_path.unlink(missing_ok=True) self._temp_report_path.unlink(missing_ok=True)

View File

@ -377,3 +377,62 @@ def test_operator_type_conditions_clear_irrelevant_bank_fields() -> None:
assert row["БИК банка получателя"] == "" assert row["БИК банка получателя"] == ""
assert row["Номер счета банка плательщика"] == "" assert row["Номер счета банка плательщика"] == ""
assert row["Номер счета банка получателя"] == "" assert row["Номер счета банка получателя"] == ""
def test_col96_territory_code_cleared_when_operation_is_not_5016() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"КодОперации": "5007",
"КодТерИнГос": "840",
},
participant_fields={},
)
assert row["Код территории"] == ""
def test_col103_and_105_fill_info_absent_when_no_employee_and_empty() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"ПризнакСотрудник": "0",
},
participant_fields={},
)
assert row["Сведения о держателе платежной карты"] == "Информация отсутствует"
assert row["Наименование иностранного банка"] == "Информация отсутствует"
def test_col103_and_105_keep_original_values_when_present() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"ПризнакСотрудник": "0",
"СведДержательКарты": "Иванов И.И.",
"НаимИнБанк": "FOREIGN BANK",
},
participant_fields={},
)
assert row["Сведения о держателе платежной карты"] == "Иванов И.И."
assert row["Наименование иностранного банка"] == "FOREIGN BANK"
def test_patronymic_supports_typo_tag_ocht() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={},
participant_fields={
"УчастникФЛИП.СведФЛИП.ФИОФЛИП.Фам": "Азимов",
"УчастникФЛИП.СведФЛИП.ФИОФЛИП.Имя": "Ислом",
"УчастникФЛИП.СведФЛИП.ФИОФЛИП.Очт": "Каримович",
},
)
assert row["Отчество"] == "Каримович"

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from pathlib import Path from pathlib import Path
from openpyxl import load_workbook from openpyxl import load_workbook
@ -15,7 +16,7 @@ def test_write_report_streaming_preserves_header_and_formats(tmp_path: Path) ->
file_name="SKO115FZ_01_123456789_20260616_X00001.xml", file_name="SKO115FZ_01_123456789_20260616_X00001.xml",
record_id="RID-1", record_id="RID-1",
operation_index=1, operation_index=1,
operation_fields={"СуммаОпер": "100.50", "ДатаОпер": "2026-06-16"}, operation_fields={"СумОперации": "100.50", "ДатаСообщения": "2026-06-16"},
participant_fields={}, participant_fields={},
) )
] ]
@ -33,26 +34,11 @@ def test_write_report_streaming_preserves_header_and_formats(tmp_path: Path) ->
] ]
assert "SKO115FZ_01_123456789_20260616_X00001.xml" in row_values assert "SKO115FZ_01_123456789_20260616_X00001.xml" in row_values
date_col_idx = next( date_col_idx = FIXED_REPORT_COLUMNS.index("Дата сообщения") + 1
( assert isinstance(sheet.cell(row=3, column=date_col_idx).value, datetime)
index
for index, column in enumerate(FIXED_REPORT_COLUMNS, start=1)
if "Дата" in column
),
None,
)
if date_col_idx is not None:
assert sheet.cell(row=3, column=date_col_idx).number_format == "DD.MM.YYYY" assert sheet.cell(row=3, column=date_col_idx).number_format == "DD.MM.YYYY"
sum_col_idx = next( sum_col_idx = FIXED_REPORT_COLUMNS.index("Сумма в валюте проведения") + 1
(
index
for index, column in enumerate(FIXED_REPORT_COLUMNS, start=1)
if "Сумм" in column or "Сумма" in column
),
None,
)
if sum_col_idx is not None:
assert sheet.cell(row=3, column=sum_col_idx).number_format == "#,##0.00" assert sheet.cell(row=3, column=sum_col_idx).number_format == "#,##0.00"
@ -77,6 +63,53 @@ def test_write_report_contains_top_group_header_row(tmp_path: Path) -> None:
assert merged_ranges == expected_ranges assert merged_ranges == expected_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() == datetime(2005, 8, 23).date()
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 _column_letter(index: int) -> str: def _column_letter(index: int) -> str:
value = index value = index
result = "" result = ""