This commit is contained in:
Raykov-MS 2026-08-11 13:08:00 +03:00
parent 9968d84e3b
commit bef38dedd8
6 changed files with 240 additions and 26 deletions

View File

@ -65,6 +65,7 @@ COL_ADDRESS_ONE_LINE = _index(141)
COL_ADDRESS_STRUCTURED = _index(142) COL_ADDRESS_STRUCTURED = _index(142)
COL_REG_PLACE_ONE_LINE = _index(152) COL_REG_PLACE_ONE_LINE = _index(152)
COL_REG_PLACE_STRUCTURED = _index(153) COL_REG_PLACE_STRUCTURED = _index(153)
COL_EIO_ADDRESS_ONE_LINE = _index(167)
# ЕИО и ценные бумаги. # ЕИО и ценные бумаги.
COL_EIO_INN = _index(163) COL_EIO_INN = _index(163)

View File

@ -20,6 +20,7 @@ from .column_constants import (
COL_CP_ISSUER_INN, COL_CP_ISSUER_INN,
COL_CURRENCY, COL_CURRENCY,
COL_CURRENCY_SIGN, COL_CURRENCY_SIGN,
COL_EIO_ADDRESS_ONE_LINE,
COL_EIO_INN, COL_EIO_INN,
COL_EIO_REG_PLACE_ONE_LINE, COL_EIO_REG_PLACE_ONE_LINE,
COL_EMPLOYEE_SIGN, COL_EMPLOYEE_SIGN,
@ -330,6 +331,7 @@ def build_fixed_row_by_index(
"УчастникЮЛ.БенефициарЮЛ.", "УчастникЮЛ.БенефициарЮЛ.",
"УчастникФЛИП.БенефициарФЛИП.", "УчастникФЛИП.БенефициарФЛИП.",
"УчастникИНБОЮЛ.БенефициарИНБОЮЛ.", "УчастникИНБОЮЛ.БенефициарИНБОЮЛ.",
"УчастникИНБОЮЛ.СведИНБОЮЛ.Учредитель.",
"СведЕИО.", "СведЕИО.",
"БенефициарЮЛ.", "БенефициарЮЛ.",
"БенефициарФЛИП.", "БенефициарФЛИП.",
@ -378,6 +380,9 @@ def build_fixed_row_by_index(
) )
if not is_continuation: if not is_continuation:
_apply_participant_identity_rules(row, participant_fields) _apply_participant_identity_rules(row, participant_fields)
# Сначала убираем запрещённые однострочные адреса, чтобы структурная
# группа не очистила их компоненты до проверки резидентства.
_rule_address_one_line(row)
_apply_structured_group_rules(row) _apply_structured_group_rules(row)
if not is_continuation: if not is_continuation:
_apply_business_rules( _apply_business_rules(
@ -515,7 +520,7 @@ def _normalize_indexed_path(path: str) -> str:
def _is_eio_path(path: str) -> bool: def _is_eio_path(path: str) -> bool:
normalized = _normalize_indexed_path(path) normalized = _normalize_indexed_path(path)
return any( return ".СведИНБОЮЛ.Учредитель." in f".{normalized}." or any(
f".{block_name}." in f".{normalized}." for block_name in _EIO_BLOCK_NAMES f".{block_name}." in f".{normalized}." for block_name in _EIO_BLOCK_NAMES
) )
@ -862,10 +867,11 @@ def _allow_one_line_address(row: dict[int, str]) -> bool:
def _rule_address_one_line(row: dict[int, str]) -> None: def _rule_address_one_line(row: dict[int, str]) -> None:
"""Правила 141, 152 и 178: однострочные адреса только для нерезидентов.""" """Правила 141, 152, 167 и 178: однострочные адреса только для нерезидентов."""
if not _allow_one_line_address(row): if not _allow_one_line_address(row):
_set(row, COL_ADDRESS_ONE_LINE, "") _set(row, COL_ADDRESS_ONE_LINE, "")
_set(row, COL_REG_PLACE_ONE_LINE, "") _set(row, COL_REG_PLACE_ONE_LINE, "")
_set(row, COL_EIO_ADDRESS_ONE_LINE, "")
_set(row, COL_EIO_REG_PLACE_ONE_LINE, "") _set(row, COL_EIO_REG_PLACE_ONE_LINE, "")

View File

@ -3322,18 +3322,18 @@
{ {
"index": 167, "index": 167,
"column_id": "legacy_167", "column_id": "legacy_167",
"column_name": "Адрес ЮЛ/ Адрес места жительства (места нахождения) ЕИО/Бенефициара (по структуре полный)", "column_name": "Адрес ЮЛ/ Адрес места жительства (места нахождения) ЕИО/Бенефициара (одной строкой)",
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "АдресСтрока|АдрУчредитель", "xml_tag": "АдресСтрока|АдрУчредитель",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/АдресСтрока", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/АдресСтрока",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": true, "allow_short_lookup": true,
"allow_direct_mapping": true, "allow_direct_mapping": true,
"structured_value": true, "structured_value": false,
"structured_group": "", "structured_group": "legacy_168",
"structured_role": "", "structured_role": "one_line",
"structured_order": 0, "structured_order": 1,
"join_with_space": false, "join_with_space": false,
"multiplies": "нет", "multiplies": "нет",
"required": "Обязательно", "required": "Обязательно",
@ -3346,7 +3346,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Индекс|КодОКСМ|КодСубъектаПоОКАТО|Район|Пункт|Улица|Дом|Корп|Оф|АдрУчредитель", "xml_tag": "Индекс|КодОКСМ|КодСубъектаПоОКАТО|Район|Пункт|Улица|Дом|Корп|Оф|АдрУчредитель",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": true, "allow_short_lookup": true,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3366,7 +3366,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Индекс", "xml_tag": "Индекс",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Индекс", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Индекс",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3386,7 +3386,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "КодОКСМ", "xml_tag": "КодОКСМ",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодОКСМ", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/КодОКСМ",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3406,7 +3406,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "КодСубъектаПоОКАТО", "xml_tag": "КодСубъектаПоОКАТО",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодСубъектаПоОКАТО", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/КодСубъектаПоОКАТО",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3426,7 +3426,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Район", "xml_tag": "Район",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Район", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Район",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3446,7 +3446,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Пункт", "xml_tag": "Пункт",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Пункт", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Пункт",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3466,7 +3466,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Улица", "xml_tag": "Улица",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Улица", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Улица",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3486,7 +3486,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Дом", "xml_tag": "Дом",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Дом", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Дом",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3506,7 +3506,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Корп", "xml_tag": "Корп",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Корп", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Корп",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,
@ -3526,7 +3526,7 @@
"block": "eio", "block": "eio",
"report_group": "eio", "report_group": "eio",
"xml_tag": "Оф", "xml_tag": "Оф",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Оф", "xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Оф",
"source_scope": "participant", "source_scope": "participant",
"allow_short_lookup": false, "allow_short_lookup": false,
"allow_direct_mapping": true, "allow_direct_mapping": true,

View File

@ -79,8 +79,9 @@ def parse_xml_content(file_name: str, xml_content: bytes | str) -> FileParseResu
operation, excluded_tags={"УчастникОп"} operation, excluded_tags={"УчастникОп"}
) )
operation_fields = {**common_fields, **operation_fields} operation_fields = {**common_fields, **operation_fields}
operation_fields = _collapse_indexed_fields(operation_fields)
record_id = _extract_record_id(operation_fields, index) record_id = _extract_record_id(operation_fields, index)
operation_rows = _expand_indexed_fields(operation_fields) operation_rows = [operation_fields]
participants = _find_children_by_name(operation, "УчастникОп") participants = _find_children_by_name(operation, "УчастникОп")
if participants: if participants:
@ -92,7 +93,7 @@ def parse_xml_content(file_name: str, xml_content: bytes | str) -> FileParseResu
file_name=file_name, file_name=file_name,
operation_index=index, operation_index=index,
record_id=record_id, record_id=record_id,
operation_rows=operation_rows, operation_rows=operation_rows[:1],
participant_rows=participant_rows, participant_rows=participant_rows,
) )
) )
@ -141,13 +142,14 @@ def _extract_direct_fields(
fields: dict[str, str] = {} fields: dict[str, str] = {}
children = [child for child in element if _normalize_tag(child.tag) not in excluded] children = [child for child in element if _normalize_tag(child.tag) not in excluded]
counts = Counter(_normalize_tag(child.tag) for child in children) counts = Counter(_normalize_tag(child.tag) for child in children)
structural_repeats = _find_structural_repeat_tags(children, counts)
positions: Counter[str] = Counter() positions: Counter[str] = Counter()
for child in children: for child in children:
tag = _normalize_tag(child.tag) tag = _normalize_tag(child.tag)
position = positions[tag] position = positions[tag]
positions[tag] += 1 positions[tag] += 1
path = f"{tag}[{position}]" if counts[tag] > 1 else tag path = f"{tag}[{position}]" if tag in structural_repeats else tag
_collect_leaf_fields(child, path, fields) _collect_leaf_fields(child, path, fields)
return fields return fields
@ -196,6 +198,15 @@ def _build_expanded_rows(
return rows return rows
def _collapse_indexed_fields(fields: dict[str, str]) -> dict[str, str]:
"""Сохраняет все операционные повторы в одной строке без потери значений."""
collapsed: dict[str, str] = {}
for key, value in fields.items():
deindexed_key = re.sub(r"\[\d+\]", "", key)
_append_value(collapsed, deindexed_key, value)
return collapsed
def _expand_indexed_fields( def _expand_indexed_fields(
fields: dict[str, str], fields: dict[str, str],
*, *,
@ -250,6 +261,21 @@ def _expand_indexed_fields(
return [primary, *continuations] return [primary, *continuations]
def _find_structural_repeat_tags(
children: list[ET.Element],
counts: Counter[str],
) -> set[str]:
"""Возвращает повторяемые теги структурных, а не скалярных XML-узлов."""
first_by_tag: dict[str, ET.Element] = {}
for child in children:
first_by_tag.setdefault(_normalize_tag(child.tag), child)
return {
tag
for tag, first_child in first_by_tag.items()
if counts[tag] > 1 and (len(first_child) > 0 or bool(first_child.attrib))
}
def _extract_record_id(operation_fields: dict[str, str], operation_index: int) -> str: def _extract_record_id(operation_fields: dict[str, str], operation_index: int) -> str:
possible_keys = ( possible_keys = (
"ИдентификаторЗаписи", "ИдентификаторЗаписи",
@ -317,17 +343,22 @@ def _collect_leaf_fields(
_collect_attributes(element, current_path, fields) _collect_attributes(element, current_path, fields)
children = list(element) children = list(element)
if not children: if not children:
_append_value(fields, current_path, (element.text or "").strip()) text = (element.text or "").strip()
if text or not element.attrib:
_append_value(fields, current_path, text)
return return
counts = Counter(_normalize_tag(child.tag) for child in children) counts = Counter(_normalize_tag(child.tag) for child in children)
structural_repeats = _find_structural_repeat_tags(children, counts)
positions: Counter[str] = Counter() positions: Counter[str] = Counter()
for child in children: for child in children:
child_name = _normalize_tag(child.tag) child_name = _normalize_tag(child.tag)
position = positions[child_name] position = positions[child_name]
positions[child_name] += 1 positions[child_name] += 1
child_path = ( child_path = (
f"{child_name}[{position}]" if counts[child_name] > 1 else child_name f"{child_name}[{position}]"
if child_name in structural_repeats
else child_name
) )
_collect_leaf_fields(child, f"{current_path}.{child_path}", fields) _collect_leaf_fields(child, f"{current_path}.{child_path}", fields)

View File

@ -250,7 +250,7 @@ def test_structured_column_is_not_filled_directly_from_single_line_tag() -> None
assert ( assert (
row[ row[
"Адрес ЮЛ/ Адрес места жительства (места нахождения) " "Адрес ЮЛ/ Адрес места жительства (места нахождения) "
"ЕИО/Бенефициара (по структуре полный)" "ЕИО/Бенефициара (одной строкой)"
] ]
== "" == ""
) )
@ -291,12 +291,16 @@ def test_structured_column_uses_non_one_line_candidate_when_mixed_tags() -> None
}, },
) )
assert row["Адрес в одной строке"] == "" assert row["Адрес в одной строке"] == ""
assert row[
"Адрес ЮЛ/ Адрес места жительства (места нахождения) "
"ЕИО/Бенефициара (одной строкой)"
] == ("г. Москва, ул. Пушкина, д. 1")
assert ( assert (
row[ row[
"Адрес ЮЛ/ Адрес места жительства (места нахождения) " "Адрес ЮЛ/ Адрес места жительства (места нахождения) "
"ЕИО/Бенефициара (по структуре полный)" "ЕИО/Бенефициара (структура полностью)"
] ]
== "г. Москва, ул. Пушкина, д. 1" == ""
) )
@ -492,6 +496,70 @@ def test_one_line_address_columns_cleared_when_participant_is_resident_client()
) )
def test_eio_one_line_address_obeys_participant_residency_rule() -> None:
participant_fields = {
"УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.АдресСтрока": "г. Москва, ул. Тестовая",
"УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.Индекс": "101000",
"УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.КодОКСМ": "643",
}
resident_client = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ПризнУчастника": "1", "ПризнКлиент": "1"},
participant_fields=participant_fields,
)
nonresident = build_fixed_row_by_index(
file_name="f.xml",
record_id="R2",
operation_index=2,
operation_fields={"ПризнУчастника": "0", "ПризнКлиент": "1"},
participant_fields=participant_fields,
)
assert resident_client[167] == ""
assert resident_client[169] == "101000"
assert resident_client[170] == "643"
assert nonresident[167] == "г. Москва, ул. Тестовая"
def test_foreign_structure_founder_address_populates_eio_address_block() -> None:
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={},
participant_fields={
"УчастникИНБОЮЛ.СведИНБОЮЛ.Учредитель.АдрУчредитель.Индекс": "101000",
"УчастникИНБОЮЛ.СведИНБОЮЛ.Учредитель.АдрУчредитель.КодОКСМ": "643",
"УчастникИНБОЮЛ.СведИНБОЮЛ.Учредитель.АдрУчредитель.Пункт": "Москва",
},
)
assert row[167] == ""
assert row[168] == "101000, 643, Москва"
assert row[169] == "101000"
assert row[170] == "643"
assert row[173] == "Москва"
def test_foreign_structure_founder_one_line_address_activates_eio_block() -> None:
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ПризнУчастника": "0"},
participant_fields={
"УчастникИНБОЮЛ.СведИНБОЮЛ.Учредитель.АдрУчредитель.АдресСтрока": (
"г. Москва, ул. Пушкина, д. 1"
),
},
)
assert row[167] == "г. Москва, ул. Пушкина, д. 1"
assert row[168] == ""
def test_okato_cleared_when_country_code_is_not_russia() -> None: def test_okato_cleared_when_country_code_is_not_russia() -> None:
row = build_fixed_row( row = build_fixed_row(
file_name="f.xml", file_name="f.xml",

View File

@ -157,6 +157,27 @@ def test_extract_direct_fields_keeps_single_sibling_path_unindexed() -> None:
assert fields == {"Документ.НомДок": "DOC-1"} assert fields == {"Документ.НомДок": "DOC-1"}
def test_parse_xml_separates_repeated_attribute_only_structures() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP-ATTR</ИдентификаторЗаписи>
<УчастникОп>
<ТипУчастника>1</ТипУчастника>
<Документ НомДок="DOC-1"/>
<Документ НомДок="DOC-2"/>
</УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert len(result.rows) == 2
assert result.rows[0].participant_fields["Документ.НомДок"] == "DOC-1"
assert result.rows[1].participant_fields == {"Документ.НомДок": "DOC-2"}
def test_parse_xml_expands_documents_and_eio_into_additive_rows() -> None: def test_parse_xml_expands_documents_and_eio_into_additive_rows() -> None:
xml = _base_xml( xml = _base_xml(
""" """
@ -224,3 +245,90 @@ def test_parse_xml_separates_single_eio_and_single_beneficiary() -> None:
assert result.rows[1].participant_fields == { assert result.rows[1].participant_fields == {
"УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП": "111111111111" "УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП": "111111111111"
} }
def test_operation_repeats_are_collapsed_once_for_two_participants() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP8</ИдентификаторЗаписи>
<ОснованиеОп><НомДок>BASE-1</НомДок></ОснованиеОп>
<ОснованиеОп><НомДок>BASE-2</НомДок></ОснованиеОп>
<УчастникОп><ТипУчастника>1</ТипУчастника></УчастникОп>
<УчастникОп><ТипУчастника>2</ТипУчастника></УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert len(result.rows) == 2
assert [row.participant_fields.get("ТипУчастника") for row in result.rows] == [
"1",
"2",
]
for row in result.rows:
assert row.operation_fields["ОснованиеОп.НомДок"] == "BASE-1; BASE-2"
def test_operation_repeats_are_collapsed_for_single_participant() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP9</ИдентификаторЗаписи>
<ОснованиеОп><НомДок>BASE-1</НомДок></ОснованиеОп>
<ОснованиеОп><НомДок>BASE-2</НомДок></ОснованиеОп>
<УчастникОп><ТипУчастника>1</ТипУчастника></УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert len(result.rows) == 1
assert result.rows[0].operation_fields["ОснованиеОп.НомДок"] == "BASE-1; BASE-2"
def test_mixed_operation_and_eio_repeats_are_additive_not_cartesian() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP10</ИдентификаторЗаписи>
<ОснованиеОп><НомДок>BASE-1</НомДок></ОснованиеОп>
<ОснованиеОп><НомДок>BASE-2</НомДок></ОснованиеОп>
<УчастникОп>
<ТипУчастника>1</ТипУчастника>
<УчастникЮЛ>
<СведЕИО><ФЛЕИО><ИННФЛИП>111111111111</ИННФЛИП></ФЛЕИО></СведЕИО>
<СведЕИО><ФЛЕИО><ИННФЛИП>222222222222</ИННФЛИП></ФЛЕИО></СведЕИО>
</УчастникЮЛ>
</УчастникОп>
<УчастникОп><ТипУчастника>2</ТипУчастника></УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert len(result.rows) == 3
assert result.rows[0].participant_fields["ТипУчастника"] == "1"
assert result.rows[1].participant_fields == {
"УчастникЮЛ.СведЕИО.ФЛЕИО.ИННФЛИП": "222222222222"
}
assert result.rows[2].participant_fields["ТипУчастника"] == "2"
assert result.rows[0].operation_fields["ОснованиеОп.НомДок"] == "BASE-1; BASE-2"
def test_repeated_scalar_leaves_are_joined_without_continuation_rows() -> None:
operation = ET.fromstring(
"""
<Операция>
<ПризнНеобОперации>1106</ПризнНеобОперации>
<ПризнНеобОперации>1499</ПризнНеобОперации>
</Операция>
"""
)
fields = _extract_direct_fields(operation)
assert fields == {"ПризнНеобОперации": "1106; 1499"}