full rule

This commit is contained in:
Raykov-MS 2026-07-31 11:37:58 +03:00
parent 55f0312929
commit eee68ab4e2
4 changed files with 212 additions and 3 deletions

View File

@ -95,6 +95,7 @@ def _build_rules(mapping_data: list[dict[str, str | int]]) -> tuple[MappingRule,
"ФИО ( в одной строке)", "ФИО ( в одной строке)",
} }
for row in mapping_data: for row in mapping_data:
index = int(row.get("index", 0))
column = str(row.get("column_name", "")).strip() column = str(row.get("column_name", "")).strip()
tag = str(row.get("xml_tag", "")).strip() tag = str(row.get("xml_tag", "")).strip()
path = str(row.get("xml_path", "")).strip() path = str(row.get("xml_path", "")).strip()
@ -133,7 +134,12 @@ def _build_rules(mapping_data: list[dict[str, str | int]]) -> tuple[MappingRule,
candidate_keys=tuple(direct_candidates), candidate_keys=tuple(direct_candidates),
source=( source=(
"participant" "participant"
if column in participant_columns or column.startswith("ИНН") if (
column in participant_columns
or column.startswith("ИНН")
or index in range(128, 140)
or index in range(180, 193)
)
else "any" else "any"
), ),
allow_direct_mapping=allow_direct_mapping, allow_direct_mapping=allow_direct_mapping,
@ -338,6 +344,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) _apply_conditional_rules_75_123(row)
_apply_conditional_rules_124_203(row)
for column in FIXED_REPORT_COLUMNS: for column in FIXED_REPORT_COLUMNS:
row.setdefault(column, "") row.setdefault(column, "")
return row return row
@ -551,3 +558,49 @@ def _apply_conditional_rules_75_123(row: dict[str, str]) -> None:
# 105. Аналогично для наименования иностранного банка. # 105. Аналогично для наименования иностранного банка.
if _row_value(row, 104) == "0" and not _row_value(row, 105): if _row_value(row, 104) == "0" and not _row_value(row, 105):
_set_row_value(row, 105, "Информация отсутствует") _set_row_value(row, 105, "Информация отсутствует")
def _allow_one_line_address(row: dict[str, str]) -> bool:
resident_flag = _row_value(row, 110)
client_flag = _row_value(row, 111)
if not resident_flag and not client_flag:
return True
return resident_flag in {"0", "9"} or (resident_flag == "1" and client_flag == "0")
def _apply_conditional_rules_124_203(row: dict[str, str]) -> None:
participant_type = _row_value(row, 109)
# 124. Для ФЛ и ИП показатель отсутствует.
if participant_type in {"2", "3"}:
_set_row_value(row, 124, "")
# 125. Для ИП СНИЛС не выводится.
if participant_type == "3":
insurance_value = _row_value(row, 125)
if re.fullmatch(r"\d{3}-?\d{3}-?\d{3}\s?\d{2}", insurance_value):
_set_row_value(row, 125, "")
# 140/151/177. Однострочные адреса только для нерезидента,
# неопределенного резидентства или резидента-не клиента.
if not _allow_one_line_address(row):
_set_row_value(row, 141, "")
_set_row_value(row, 152, "")
_set_row_value(row, 178, "")
# Правило КодОКСМ/КодОКАТО в адресных блоках:
# ОКАТО заполняется только при российском коде страны 643.
okato_country_pairs = (
(52, 53),
(67, 68),
(86, 87),
(144, 145),
(170, 171),
)
for country_col, okato_col in okato_country_pairs:
if _row_value(row, country_col) != "643":
_set_row_value(row, okato_col, "")
# 162. Для ФЛ показатель отсутствует.
if participant_type == "2":
_set_row_value(row, 163, "")

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import re
import shutil import shutil
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, datetime from datetime import date, datetime
@ -60,11 +61,11 @@ TOP_GROUP_HEADERS: tuple[GroupHeader, ...] = (
), ),
GroupHeader( GroupHeader(
start_col=87, start_col=87,
end_col=121, end_col=126,
title="Участник операции (базовые сведения)", title="Участник операции (базовые сведения)",
), ),
GroupHeader( GroupHeader(
start_col=122, start_col=127,
end_col=154, end_col=154,
title="Участник операции (идентификация, документы, адреса)", title="Участник операции (идентификация, документы, адреса)",
), ),
@ -148,6 +149,12 @@ class StreamingReportWriter:
self.sheet.write_datetime( self.sheet.write_datetime(
self._next_data_row, index, value, cell_format self._next_data_row, index, value, cell_format
) )
elif self._is_text_identifier_column(column_name):
self.sheet.write_string(
self._next_data_row,
index,
self._normalize_identifier_string(value),
)
elif cell_format is not None: 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:
@ -209,6 +216,18 @@ class StreamingReportWriter:
return raw_value, None return raw_value, None
return raw_value, default_format return raw_value, default_format
def _is_text_identifier_column(self, column_name: str) -> bool:
keywords = ("ИНН", "ОГРН", "КПП", "БИК", "СВИФТ")
return any(keyword in column_name for keyword in keywords)
def _normalize_identifier_string(self, value: object) -> str:
if value is None:
return ""
text = str(value).strip()
if re.fullmatch(r"\d+\.0+", text):
return text.split(".", maxsplit=1)[0]
return text
def _parse_date_value(self, value: object) -> datetime | None: def _parse_date_value(self, value: object) -> datetime | None:
if isinstance(value, datetime): if isinstance(value, datetime):
return value return value

View File

@ -436,3 +436,115 @@ def test_patronymic_supports_typo_tag_ocht() -> None:
}, },
) )
assert row["Отчество"] == "Каримович" assert row["Отчество"] == "Каримович"
def test_col124_cleared_for_physical_person_and_ip() -> None:
row_fl = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ТипФЛЧастнаяПрактика": "1"},
participant_fields={"ТипУчастника": "2"},
)
assert row_fl["Признак филиала: / Тип ФЛЧП"] == ""
row_ip = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ТипФЛЧастнаяПрактика": "1"},
participant_fields={"ТипУчастника": "3"},
)
assert row_ip["Признак филиала: / Тип ФЛЧП"] == ""
def test_col125_snils_cleared_for_ip() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={},
participant_fields={"ТипУчастника": "3", "СНИЛСФЛ": "123-456-789 00"},
)
assert row["ОМС/СНИЛС/Номер телефона"] == ""
def test_one_line_address_columns_cleared_when_participant_is_resident_client() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"ПризнУчастника": "1",
"ПризнКлиент": "1",
"АдресСтрока": "г. Москва, ул. Тестовая, д. 1",
},
participant_fields={},
)
assert row["Адрес в одной строке"] == ""
assert row["Место государственной регистрации ( в одной строке)"] == ""
assert (
row["Место государственной регистрации ЕИО/Бенефициара (одной строкой)"] == ""
)
def test_okato_cleared_when_country_code_is_not_russia() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"КодОКСМ": "840",
"КодСубъектаПоОКАТО": "45",
},
participant_fields={},
)
assert row["Код ОКАТО"] == ""
assert row["ОКАТО"] == ""
def test_col163_ogrn_eio_benef_cleared_for_physical_person() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ОГРНИП": "123456789012345"},
participant_fields={"ТипУчастника": "2"},
)
assert row["ОГРН ЕИО/Бенефициара"] == ""
def test_dul_sign_for_legal_entity_does_not_use_operation_doc_fields() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ДокУдЛичн": "451099", "ТипУчастника": "1"},
participant_fields={},
)
assert row["Признак ДУЛ"] == ""
def test_dul_columns_128_134_keep_expected_positions() -> None:
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={},
participant_fields={
"ВидДокКод": "21",
"СерияДок": "0104",
"НомДок": "476635",
"КемВыданДок": "ОВД",
"КодПодр": "222-022",
"ДатВыдачиДок": "23/08/2005",
"ДокУдЛичн": "1",
},
)
assert row["ДУЛ (структура полностью)"] == "1, 21, 0104, 476635, ОВД, 222-022"
assert row["Признак ДУЛ"] == "1"
assert row["Код вида документа/Наименование"] == "21"
assert row["Серия"] == "0104"
assert row["Номер"] == "476635"
assert row["Орган, выдавший документ"] == "ОВД"
assert row["КП"] == "222-022"

View File

@ -110,6 +110,31 @@ def test_write_report_keeps_invalid_date_as_text_without_date_format(
assert cell.number_format != "DD.MM.YYYY" 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 _column_letter(index: int) -> str: def _column_letter(index: int) -> str:
value = index value = index
result = "" result = ""