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]
|
||||
if column == "Тип участника":
|
||||
candidates = ["Тип", "ТипУчастника", *candidates]
|
||||
if column == "Отчество":
|
||||
# В некоторых выгрузках встречается опечатка тега "Очт".
|
||||
candidates = ["Отч", "Очт", *candidates]
|
||||
if path:
|
||||
for path_tag in _extract_path_candidate_keys(path):
|
||||
if path_tag not in candidates:
|
||||
@ -334,6 +337,7 @@ def build_fixed_row(
|
||||
)
|
||||
_apply_structured_group_rules(row)
|
||||
_apply_conditional_rules_1_74(row)
|
||||
_apply_conditional_rules_75_123(row)
|
||||
for column in FIXED_REPORT_COLUMNS:
|
||||
row.setdefault(column, "")
|
||||
return row
|
||||
@ -531,3 +535,19 @@ def _apply_conditional_rules_1_74(row: dict[str, str]) -> None:
|
||||
# 62. Статус перевода заполняется только для видов 2/5/8.
|
||||
if transfer_type not in {"2", "5", "8"}:
|
||||
_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
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Iterable
|
||||
@ -135,13 +136,19 @@ class StreamingReportWriter:
|
||||
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:
|
||||
default_format = self._column_formats[index]
|
||||
value, cell_format = self._normalize_cell_value(
|
||||
column_name=column_name,
|
||||
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)
|
||||
else:
|
||||
self.sheet.write(self._next_data_row, index, value)
|
||||
@ -186,6 +193,40 @@ class StreamingReportWriter:
|
||||
return self._sum_format
|
||||
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:
|
||||
try:
|
||||
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["Номер счета банка получателя"] == ""
|
||||
|
||||
|
||||
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 datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
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",
|
||||
record_id="RID-1",
|
||||
operation_index=1,
|
||||
operation_fields={"СуммаОпер": "100.50", "ДатаОпер": "2026-06-16"},
|
||||
operation_fields={"СумОперации": "100.50", "ДатаСообщения": "2026-06-16"},
|
||||
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
|
||||
|
||||
date_col_idx = next(
|
||||
(
|
||||
index
|
||||
for index, column in enumerate(FIXED_REPORT_COLUMNS, start=1)
|
||||
if "Дата" in column
|
||||
),
|
||||
None,
|
||||
)
|
||||
if date_col_idx is not None:
|
||||
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 = next(
|
||||
(
|
||||
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:
|
||||
sum_col_idx = FIXED_REPORT_COLUMNS.index("Сумма в валюте проведения") + 1
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
value = index
|
||||
result = ""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user