fix
This commit is contained in:
parent
9968d84e3b
commit
bef38dedd8
@ -65,6 +65,7 @@ COL_ADDRESS_ONE_LINE = _index(141)
|
||||
COL_ADDRESS_STRUCTURED = _index(142)
|
||||
COL_REG_PLACE_ONE_LINE = _index(152)
|
||||
COL_REG_PLACE_STRUCTURED = _index(153)
|
||||
COL_EIO_ADDRESS_ONE_LINE = _index(167)
|
||||
|
||||
# ЕИО и ценные бумаги.
|
||||
COL_EIO_INN = _index(163)
|
||||
|
||||
@ -20,6 +20,7 @@ from .column_constants import (
|
||||
COL_CP_ISSUER_INN,
|
||||
COL_CURRENCY,
|
||||
COL_CURRENCY_SIGN,
|
||||
COL_EIO_ADDRESS_ONE_LINE,
|
||||
COL_EIO_INN,
|
||||
COL_EIO_REG_PLACE_ONE_LINE,
|
||||
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:
|
||||
_apply_participant_identity_rules(row, participant_fields)
|
||||
# Сначала убираем запрещённые однострочные адреса, чтобы структурная
|
||||
# группа не очистила их компоненты до проверки резидентства.
|
||||
_rule_address_one_line(row)
|
||||
_apply_structured_group_rules(row)
|
||||
if not is_continuation:
|
||||
_apply_business_rules(
|
||||
@ -515,7 +520,7 @@ def _normalize_indexed_path(path: str) -> str:
|
||||
|
||||
def _is_eio_path(path: str) -> bool:
|
||||
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
|
||||
)
|
||||
|
||||
@ -862,10 +867,11 @@ def _allow_one_line_address(row: dict[int, str]) -> bool:
|
||||
|
||||
|
||||
def _rule_address_one_line(row: dict[int, str]) -> None:
|
||||
"""Правила 141, 152 и 178: однострочные адреса только для нерезидентов."""
|
||||
"""Правила 141, 152, 167 и 178: однострочные адреса только для нерезидентов."""
|
||||
if not _allow_one_line_address(row):
|
||||
_set(row, COL_ADDRESS_ONE_LINE, "")
|
||||
_set(row, COL_REG_PLACE_ONE_LINE, "")
|
||||
_set(row, COL_EIO_ADDRESS_ONE_LINE, "")
|
||||
_set(row, COL_EIO_REG_PLACE_ONE_LINE, "")
|
||||
|
||||
|
||||
|
||||
@ -3322,18 +3322,18 @@
|
||||
{
|
||||
"index": 167,
|
||||
"column_id": "legacy_167",
|
||||
"column_name": "Адрес ЮЛ/ Адрес места жительства (места нахождения) ЕИО/Бенефициара (по структуре полный)",
|
||||
"column_name": "Адрес ЮЛ/ Адрес места жительства (места нахождения) ЕИО/Бенефициара (одной строкой)",
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "АдресСтрока|АдрУчредитель",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/АдресСтрока",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/АдресСтрока",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": true,
|
||||
"allow_direct_mapping": true,
|
||||
"structured_value": true,
|
||||
"structured_group": "",
|
||||
"structured_role": "",
|
||||
"structured_order": 0,
|
||||
"structured_value": false,
|
||||
"structured_group": "legacy_168",
|
||||
"structured_role": "one_line",
|
||||
"structured_order": 1,
|
||||
"join_with_space": false,
|
||||
"multiplies": "нет",
|
||||
"required": "Обязательно",
|
||||
@ -3346,7 +3346,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Индекс|КодОКСМ|КодСубъектаПоОКАТО|Район|Пункт|Улица|Дом|Корп|Оф|АдрУчредитель",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": true,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3366,7 +3366,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Индекс",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Индекс",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Индекс | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Индекс",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3386,7 +3386,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "КодОКСМ",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодОКСМ",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/КодОКСМ",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3406,7 +3406,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "КодСубъектаПоОКАТО",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодСубъектаПоОКАТО",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/КодСубъектаПоОКАТО | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/КодСубъектаПоОКАТО",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3426,7 +3426,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Район",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Район",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Район | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Район",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3446,7 +3446,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Пункт",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Пункт",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Пункт | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Пункт",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3466,7 +3466,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Улица",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Улица",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Улица | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Улица",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3486,7 +3486,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Дом",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Дом",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Дом | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Дом",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3506,7 +3506,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Корп",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Корп",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Корп | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Корп",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
@ -3526,7 +3526,7 @@
|
||||
"block": "eio",
|
||||
"report_group": "eio",
|
||||
"xml_tag": "Оф",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Оф",
|
||||
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/Оф | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/Оф",
|
||||
"source_scope": "participant",
|
||||
"allow_short_lookup": false,
|
||||
"allow_direct_mapping": true,
|
||||
|
||||
@ -79,8 +79,9 @@ def parse_xml_content(file_name: str, xml_content: bytes | str) -> FileParseResu
|
||||
operation, excluded_tags={"УчастникОп"}
|
||||
)
|
||||
operation_fields = {**common_fields, **operation_fields}
|
||||
operation_fields = _collapse_indexed_fields(operation_fields)
|
||||
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, "УчастникОп")
|
||||
|
||||
if participants:
|
||||
@ -92,7 +93,7 @@ def parse_xml_content(file_name: str, xml_content: bytes | str) -> FileParseResu
|
||||
file_name=file_name,
|
||||
operation_index=index,
|
||||
record_id=record_id,
|
||||
operation_rows=operation_rows,
|
||||
operation_rows=operation_rows[:1],
|
||||
participant_rows=participant_rows,
|
||||
)
|
||||
)
|
||||
@ -141,13 +142,14 @@ def _extract_direct_fields(
|
||||
fields: dict[str, str] = {}
|
||||
children = [child for child in element if _normalize_tag(child.tag) not in excluded]
|
||||
counts = Counter(_normalize_tag(child.tag) for child in children)
|
||||
structural_repeats = _find_structural_repeat_tags(children, counts)
|
||||
positions: Counter[str] = Counter()
|
||||
|
||||
for child in children:
|
||||
tag = _normalize_tag(child.tag)
|
||||
position = positions[tag]
|
||||
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)
|
||||
|
||||
return fields
|
||||
@ -196,6 +198,15 @@ def _build_expanded_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(
|
||||
fields: dict[str, str],
|
||||
*,
|
||||
@ -250,6 +261,21 @@ def _expand_indexed_fields(
|
||||
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:
|
||||
possible_keys = (
|
||||
"ИдентификаторЗаписи",
|
||||
@ -317,17 +343,22 @@ def _collect_leaf_fields(
|
||||
_collect_attributes(element, current_path, fields)
|
||||
children = list(element)
|
||||
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
|
||||
|
||||
counts = Counter(_normalize_tag(child.tag) for child in children)
|
||||
structural_repeats = _find_structural_repeat_tags(children, counts)
|
||||
positions: Counter[str] = Counter()
|
||||
for child in children:
|
||||
child_name = _normalize_tag(child.tag)
|
||||
position = positions[child_name]
|
||||
positions[child_name] += 1
|
||||
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)
|
||||
|
||||
|
||||
@ -250,7 +250,7 @@ def test_structured_column_is_not_filled_directly_from_single_line_tag() -> None
|
||||
assert (
|
||||
row[
|
||||
"Адрес ЮЛ/ Адрес места жительства (места нахождения) "
|
||||
"ЕИО/Бенефициара (по структуре полный)"
|
||||
"ЕИО/Бенефициара (одной строкой)"
|
||||
]
|
||||
== ""
|
||||
)
|
||||
@ -291,12 +291,16 @@ def test_structured_column_uses_non_one_line_candidate_when_mixed_tags() -> None
|
||||
},
|
||||
)
|
||||
assert row["Адрес в одной строке"] == ""
|
||||
assert row[
|
||||
"Адрес ЮЛ/ Адрес места жительства (места нахождения) "
|
||||
"ЕИО/Бенефициара (одной строкой)"
|
||||
] == ("г. Москва, ул. Пушкина, д. 1")
|
||||
assert (
|
||||
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:
|
||||
row = build_fixed_row(
|
||||
file_name="f.xml",
|
||||
|
||||
@ -157,6 +157,27 @@ def test_extract_direct_fields_keeps_single_sibling_path_unindexed() -> None:
|
||||
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:
|
||||
xml = _base_xml(
|
||||
"""
|
||||
@ -224,3 +245,90 @@ def test_parse_xml_separates_single_eio_and_single_beneficiary() -> None:
|
||||
assert result.rows[1].participant_fields == {
|
||||
"УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП": "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"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user