75-123 rule
This commit is contained in:
parent
25f96438b1
commit
55f0312929
@ -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, "Информация отсутствует")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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["Отчество"] == "Каримович"
|
||||||
|
|||||||
@ -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 = ""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user