This commit is contained in:
Raykov-MS 2026-08-11 01:09:07 +03:00
parent d6b3e73cb7
commit 9968d84e3b
11 changed files with 2560 additions and 54 deletions

View File

@ -14,4 +14,3 @@ repos:
rev: v0.12.7
hooks:
- id: ruff
- id: ruff-format

View File

@ -102,6 +102,13 @@ class StructuredGroupRule:
_INVALID_TAGS = {"", "-", "Источник", "путь", "подразумеваются", "тэга", "нашла"}
_INDEXED_PATH_SEGMENT = re.compile(r"\[\d+\]")
_EIO_BLOCK_NAMES = (
"СведЕИО",
"БенефициарЮЛ",
"БенефициарФЛИП",
"БенефициарИНБОЮЛ",
)
def _bank_bik_candidate_keys(bank_block: str) -> tuple[str, ...]:
@ -307,6 +314,7 @@ def build_fixed_row_by_index(
operation_index: int,
operation_fields: dict[str, str],
participant_fields: dict[str, str],
is_continuation: bool = False,
) -> dict[int, str]:
row: dict[int, str] = {
COL_FILE_NAME: file_name,
@ -315,14 +323,17 @@ def build_fixed_row_by_index(
merged = _merge_fields(operation_fields, participant_fields)
merged_suffix_index = _build_suffix_index(merged)
operation_suffix_index = _build_suffix_index(operation_fields)
participant_suffix_index = _build_suffix_index(participant_fields)
has_eio_block = _has_any_prefixed_key(
participant_fields,
(
"УчастникЮЛ.СведЕИО.",
"УчастникЮЛ.БенефициарЮЛ.",
"УчастникФЛИП.БенефициарФЛИП.",
"УчастникИНБОЮЛ.БенефициарИНБОЮЛ.",
"СведЕИО.",
"БенефициарЮЛ.",
"БенефициарФЛИП.",
"БенефициарИНБОЮЛ.",
),
)
has_cp_block = _has_any_prefixed_key(
@ -339,8 +350,11 @@ def build_fixed_row_by_index(
row[rule.column_index] = ""
continue
if rule.source == "participant":
source_payload = participant_fields
suffix_index = participant_suffix_index
source_payload = _scope_participant_payload(
participant_fields,
eio_only=rule.column_index in BLOCK_EIO,
)
suffix_index = _build_suffix_index(source_payload)
elif rule.source == "operation":
source_payload = operation_fields
suffix_index = operation_suffix_index
@ -362,13 +376,17 @@ def build_fixed_row_by_index(
rule.candidate_keys,
allow_short_lookup=rule.allow_short_lookup,
)
_apply_participant_identity_rules(row, participant_fields)
if not is_continuation:
_apply_participant_identity_rules(row, participant_fields)
_apply_structured_group_rules(row)
_apply_business_rules(
row,
has_eio_block=has_eio_block,
has_cp_block=has_cp_block,
)
if not is_continuation:
_apply_business_rules(
row,
has_eio_block=has_eio_block,
has_cp_block=has_cp_block,
)
else:
_apply_continuation_validation_rules(row)
for meta in COLUMNS.ordered_columns():
row.setdefault(meta.index, "")
return row
@ -381,6 +399,7 @@ def build_fixed_row(
operation_index: int,
operation_fields: dict[str, str],
participant_fields: dict[str, str],
is_continuation: bool = False,
) -> dict[str, str]:
by_index = build_fixed_row_by_index(
file_name=file_name,
@ -388,6 +407,7 @@ def build_fixed_row(
operation_index=operation_index,
operation_fields=operation_fields,
participant_fields=participant_fields,
is_continuation=is_continuation,
)
by_name: dict[str, str] = {}
for meta in COLUMNS.ordered_columns():
@ -412,7 +432,6 @@ def _apply_structured_group_rules(row: dict[int, str]) -> None:
if _get(row, component_column_index).strip()
]
if not values:
_set(row, rule.structured_column, "")
continue
separator = " " if rule.join_with_space else ", "
@ -432,13 +451,18 @@ def _merge_fields(
def _has_payload_block(payload: dict[str, str], block_name: str) -> bool:
prefix = f"{block_name}."
nested_marker = f".{block_name}."
return any(key.startswith(prefix) or nested_marker in key for key in payload)
return any(
(normalized := _normalize_indexed_path(key)).startswith(prefix)
or nested_marker in normalized
for key in payload
)
def _pick_scoped_payload_value(payload: dict[str, str], scoped_path: str) -> str:
nested_suffix = f".{scoped_path}"
for key, value in payload.items():
if value and (key == scoped_path or key.endswith(nested_suffix)):
normalized = _normalize_indexed_path(key)
if value and (normalized == scoped_path or normalized.endswith(nested_suffix)):
return str(value).strip()
return ""
@ -478,7 +502,33 @@ def _apply_participant_identity_rules(
def _has_any_prefixed_key(payload: dict[str, str], prefixes: tuple[str, ...]) -> bool:
return any(any(key.startswith(prefix) for prefix in prefixes) for key in payload)
return any(
any(_normalize_indexed_path(key).startswith(prefix) for prefix in prefixes)
for key in payload
)
def _normalize_indexed_path(path: str) -> str:
"""Убирает индексы повторов, сохраняя логическую структуру XML-пути."""
return _INDEXED_PATH_SEGMENT.sub("", path)
def _is_eio_path(path: str) -> bool:
normalized = _normalize_indexed_path(path)
return any(
f".{block_name}." in f".{normalized}." for block_name in _EIO_BLOCK_NAMES
)
def _scope_participant_payload(
payload: dict[str, str],
*,
eio_only: bool,
) -> dict[str, str]:
"""Изолирует поля участника от вложенного блока ЕИО/бенефициара."""
return {
key: value for key, value in payload.items() if _is_eio_path(key) is eio_only
}
def _build_suffix_index(payload: dict[str, str]) -> dict[str, str]:
@ -501,6 +551,14 @@ def _pick_value(
for key in candidates:
if value := payload.get(key):
return value
normalized_key = _normalize_indexed_path(key)
for payload_key, value in payload.items():
normalized_payload_key = _normalize_indexed_path(payload_key)
if value and (
normalized_payload_key == normalized_key
or normalized_payload_key.endswith(f".{normalized_key}")
):
return value
if not allow_short_lookup:
continue
short = key.rsplit(".", maxsplit=1)[-1]
@ -613,6 +671,13 @@ def _apply_business_rules(
_clear_columns(row, BLOCK_CP)
def _apply_continuation_validation_rules(row: dict[int, str]) -> None:
"""Проверяет локальные коды повтора без подстановки значений основной строки."""
_rule_ftr_sign(row)
_rule_suspension_basis(row)
_rule_esp_sign(row)
def _rule_ftr_sign(row: dict[int, str]) -> None:
"""Правило 10: оставляем только допустимые значения признака ФТР."""
if _get(row, COL_FTR_SIGN) not in VALID_FTR_SIGNS:

View File

@ -3126,7 +3126,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "НаимЮЛ/ФИОФЛИП/НаимУчредитель",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЮЛ/НаимЮЛ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ФИОФЛИП/Фам | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ФИОФЛИП/Имя | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ФИОФЛИП/Отч | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/НаимУчредитель/ЮрЛицо | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/НаимУчредитель/ФизЛицо/Фам | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/НаимУчредитель/ФизЛицо/Имя | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/НаимУчредитель/ФизЛицо/Отч",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/НаимЮЛ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ФИОФЛИП/Фам | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ФИОФЛИП/Имя | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ФИОФЛИП/Отч | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ФИОФЛИП/Фам | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ФИОФЛИП/Имя | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ФИОФЛИП/Отч",
"source_scope": "participant",
"allow_short_lookup": true,
"allow_direct_mapping": true,
@ -3146,7 +3146,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "ФИОФЛИП",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ФИОФЛИП/ФИОСтрока",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ФИОФЛИП/ФИОСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ФИОФЛИП/ФИОСтрока",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -3166,7 +3166,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "Фам",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ФИОФЛИП/Фам",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ФИОФЛИП/Фам | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ФИОФЛИП/Фам",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -3186,7 +3186,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "Имя",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ФИОФЛИП/Имя",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ФИОФЛИП/Имя | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ФИОФЛИП/Имя",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -3206,7 +3206,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "Отч",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ФИОФЛИП/Отч",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ФИОФЛИП/Отч | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ФИОФЛИП/Отч",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -3226,14 +3226,14 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "ИННЮЛ/ИННФЛИП",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЮЛ/ИННЮЛ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ИННФЛИП",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/ИННЮЛ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ИННФЛИП | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ИННФЛИП",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
"structured_value": false,
"structured_group": "legacy_157",
"structured_role": "component",
"structured_order": 4,
"structured_group": "",
"structured_role": "",
"structured_order": 0,
"join_with_space": false,
"multiplies": "нет",
"required": "обязательно",
@ -3246,7 +3246,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "ОГРНЮЛ|ОГРНИП|ТипФЛЧастнаяПрактика",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЮЛ/ОГРНЮЛ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ОГРНИП | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ТипФЛЧастнаяПрактика",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/ОГРНЮЛ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ОГРНИП | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ОГРНИП | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ТипФЛЧастнаяПрактика | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ТипФЛЧастнаяПрактика",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -3285,10 +3285,10 @@
"column_name": "Код страны гражданства ЕИО/Бенефициара",
"block": "eio",
"report_group": "eio",
"xml_tag": "",
"xml_path": "",
"source_scope": "any",
"allow_short_lookup": true,
"xml_tag": "КодОКСМ",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/КодОКСМ | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/КодОКСМ",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
"structured_value": false,
"structured_group": "",
@ -3306,7 +3306,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "ПризнакПубЛицо",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникФЛИП/СведФЛИП/ПризнакПубЛицо",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/ПризнакПубЛицо | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/ПризнакПубЛицо",
"source_scope": "participant",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -3326,7 +3326,7 @@
"block": "eio",
"report_group": "eio",
"xml_tag": "АдресСтрока|АдрУчредитель",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЮЛ/АдрРегЮЛ/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникИНБОЮЛ/СведИНБОЮЛ/Учредитель/АдрУчредитель/АдресСтрока",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ЮЛЕИО/АдрЮЛ/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/СведЕИО/ФЛЕИО/АдрРег/АдресСтрока | /СообщОперКО/ИнформЧасть/СведКО/Операция/УчастникОП/УчастникЮЛ/БенефициарЮЛ/ФЛБенефициар/АдрРег/АдресСтрока",
"source_scope": "participant",
"allow_short_lookup": true,
"allow_direct_mapping": true,
@ -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,

View File

@ -1,11 +1,21 @@
from __future__ import annotations
import re
import xml.etree.ElementTree as ET
from collections import Counter
from dataclasses import dataclass, field
from typing import Iterable
from .config import validate_file_name as validate_pipeline_file_name
_INDEXED_SEGMENT = re.compile(r"(?P<segment>[^.]+\[(?P<index>\d+)\])")
_EIO_SEGMENT = re.compile(
r"(?P<segment>"
r"(?:СведЕИО|БенефициарЮЛ|БенефициарФЛИП|БенефициарИНБОЮЛ)"
r"(?:\[(?P<index>\d+)\])?"
r")"
)
@dataclass
class ParticipantRow:
@ -14,6 +24,7 @@ class ParticipantRow:
record_id: str
operation_fields: dict[str, str]
participant_fields: dict[str, str]
is_continuation: bool = False
@dataclass
@ -69,28 +80,30 @@ def parse_xml_content(file_name: str, xml_content: bytes | str) -> FileParseResu
)
operation_fields = {**common_fields, **operation_fields}
record_id = _extract_record_id(operation_fields, index)
operation_rows = _expand_indexed_fields(operation_fields)
participants = _find_children_by_name(operation, "УчастникОп")
if participants:
for participant in participants:
participant_fields = _extract_direct_fields(participant)
result.rows.append(
ParticipantRow(
participant_rows = _expand_indexed_fields(participant_fields)
result.rows.extend(
_build_expanded_rows(
file_name=file_name,
operation_index=index,
record_id=record_id,
operation_fields=operation_fields,
participant_fields=participant_fields,
operation_rows=operation_rows,
participant_rows=participant_rows,
)
)
else:
result.rows.append(
ParticipantRow(
result.rows.extend(
_build_expanded_rows(
file_name=file_name,
operation_index=index,
record_id=record_id,
operation_fields=operation_fields,
participant_fields={},
operation_rows=operation_rows,
participant_rows=[{}],
)
)
except Exception as exc: # noqa: BLE001
@ -126,16 +139,117 @@ def _extract_direct_fields(
) -> dict[str, str]:
excluded = set(excluded_tags or ())
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)
positions: Counter[str] = Counter()
for child in element:
for child in children:
tag = _normalize_tag(child.tag)
if tag in excluded:
continue
_collect_leaf_fields(child, tag, fields)
position = positions[tag]
positions[tag] += 1
path = f"{tag}[{position}]" if counts[tag] > 1 else tag
_collect_leaf_fields(child, path, fields)
return fields
def _build_expanded_rows(
*,
file_name: str,
operation_index: int,
record_id: str,
operation_rows: list[dict[str, str]],
participant_rows: list[dict[str, str]],
) -> list[ParticipantRow]:
"""Строит основную и независимые continuation-строки без декартова умножения."""
rows = [
ParticipantRow(
file_name=file_name,
operation_index=operation_index,
record_id=record_id,
operation_fields=operation_rows[0],
participant_fields=participant_rows[0],
)
]
rows.extend(
ParticipantRow(
file_name=file_name,
operation_index=operation_index,
record_id=record_id,
operation_fields=operation_fields,
participant_fields={},
is_continuation=True,
)
for operation_fields in operation_rows[1:]
)
rows.extend(
ParticipantRow(
file_name=file_name,
operation_index=operation_index,
record_id=record_id,
operation_fields={},
participant_fields=participant_fields,
is_continuation=True,
)
for participant_fields in participant_rows[1:]
)
return rows
def _expand_indexed_fields(
fields: dict[str, str],
*,
group_eio: bool = True,
) -> list[dict[str, str]]:
"""Разворачивает индексированные sibling-блоки в аддитивные наборы полей."""
base_fields: dict[str, str] = {}
families: dict[str, dict[int, dict[str, str]]] = {}
eio_occurrences: dict[str, int] = {}
for key, value in fields.items():
eio_match = _EIO_SEGMENT.search(key) if group_eio else None
match = eio_match or _INDEXED_SEGMENT.search(key)
if match is None:
base_fields[key] = value
continue
indexed_segment = match.group("segment")
family_segment = re.sub(r"\[\d+\]$", "", indexed_segment)
if eio_match is not None:
occurrence_key = f"{key[: match.start()]}{indexed_segment}"
occurrence = eio_occurrences.setdefault(
occurrence_key,
len(eio_occurrences),
)
family = "__eio_or_beneficiary__"
else:
family = f"{key[: match.start()]}{family_segment}"
occurrence = int(match.group("index"))
deindexed_key = f"{key[: match.start()]}{family_segment}{key[match.end() :]}"
families.setdefault(family, {}).setdefault(occurrence, {})[
deindexed_key
] = value
if not families:
return [base_fields]
primary = dict(base_fields)
continuations: list[dict[str, str]] = []
for occurrences in families.values():
for position, occurrence in enumerate(sorted(occurrences)):
expanded_occurrence = _expand_indexed_fields(
occurrences[occurrence],
group_eio=False,
)
if position == 0:
primary.update(expanded_occurrence[0])
continuations.extend(expanded_occurrence[1:])
else:
continuations.extend(expanded_occurrence)
return [primary, *continuations]
def _extract_record_id(operation_fields: dict[str, str], operation_index: int) -> str:
possible_keys = (
"ИдентификаторЗаписи",
@ -206,9 +320,16 @@ def _collect_leaf_fields(
_append_value(fields, current_path, (element.text or "").strip())
return
counts = Counter(_normalize_tag(child.tag) for child in children)
positions: Counter[str] = Counter()
for child in children:
child_name = _normalize_tag(child.tag)
_collect_leaf_fields(child, f"{current_path}.{child_name}", fields)
position = positions[child_name]
positions[child_name] += 1
child_path = (
f"{child_name}[{position}]" if counts[child_name] > 1 else child_name
)
_collect_leaf_fields(child, f"{current_path}.{child_path}", fields)
def _collect_attributes(

View File

@ -28,6 +28,7 @@ class ReportRow:
operation_index: int
operation_fields: dict[str, str]
participant_fields: dict[str, str]
is_continuation: bool = False
@dataclass(frozen=True)
@ -93,6 +94,7 @@ class StreamingReportWriter:
operation_index=row.operation_index,
operation_fields=row.operation_fields,
participant_fields=row.participant_fields,
is_continuation=row.is_continuation,
)
for output_index, column_name in enumerate(self.columns, start=1):
zero_based_index = output_index - 1

View File

@ -329,6 +329,7 @@ def _process_files(
operation_index=row.operation_index,
operation_fields=row.operation_fields,
participant_fields=row.participant_fields,
is_continuation=row.is_continuation,
)
)
stage_timings["report_append_seconds"] += (

View File

@ -0,0 +1,660 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:include schemaLocation="data_types_v2.xsd"/>
<xs:element name="СообщОперКО">
<xs:annotation>
<xs:documentation>
Сообщение об операциях, обязательному контролю, о подозрительных операциях, а также об операциях,
приостановленных в соответствии с пунктом 10 статьи 7 и (или) пунктом 8 статьи 7.5 Федерального закона № 115-ФЗ
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="СлужЧасть" type="СлужЧастьТип">
<xs:annotation>
<xs:documentation>Служебная часть электронного сообщения</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ИнформЧасть">
<xs:annotation>
<xs:documentation>Информационная часть электронного сообщения</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="ИнфБанк" type="Банк">
<xs:annotation>
<xs:documentation>
Информация о кредитной организации (филиале кредитной организации), передающей (передающем) сведения
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведКО" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="ПризнакПредставСвед">
<xs:annotation>
<xs:documentation>
Признак представления сведений в уполномоченный орган филиалом кредитной организации
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="0|1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ИнфФилиал" type="Филиал" minOccurs="0">
<xs:annotation>
<xs:documentation>
Информация о филиале кредитной организации, представляющем сведения
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="Операция" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Сведения об операции</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="НомерЗаписи" type="ИдентификаторЗаписи">
<xs:annotation>
<xs:documentation>Номер записи в ФЭС</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ТипЗаписи">
<xs:annotation>
<xs:documentation>Тип записи</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[1-4]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="НомерОперация" type="Т100" minOccurs="0">
<xs:annotation>
<xs:documentation>Уникальный номер операции</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ПризнФТр" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Признак операции, связанной с финансированием терроризма
или распространения оружия массового уничтожения
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-5]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ОснПриостановление" minOccurs="0">
<xs:annotation>
<xs:documentation>Основания приостановления операции</xs:documentation>
<xs:documentation>!!! Показатель введен, начиная с версии 2.3 формата ФЭС !!!</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[1-3]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ДатаОперации" type="ДатаТип">
<xs:annotation>
<xs:documentation>Дата совершения (приостановления) операции </xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ДатаВыявления" type="ДатаТип" minOccurs="0">
<xs:annotation>
<xs:documentation>Дата выявления (приостановления) операции</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="КодПризнОперации">
<xs:annotation>
<xs:documentation>Код признака операции (сделки)</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[1-3]|[5-9]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ПризнакОперацияЭСП" minOccurs="0">
<xs:annotation>
<xs:documentation>Признак совершения операции с использованием ЭСП</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[1-4]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="НаимПлатежнаяСистема1" type="Т100" minOccurs="0">
<xs:annotation>
<xs:documentation>Наименование платежной системы на стороне лица, совершающего операцию</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="НаимПлатежнаяСистема2" type="Т100" minOccurs="0">
<xs:annotation>
<xs:documentation>Наименование платежной системы на стороне получателя по операции</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ВремяТранзакцияЭСП" type="ВЧПТип" minOccurs="0">
<xs:annotation>
<xs:documentation>Время совершения операции с использованием ЭСП</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="КодОперации">
<xs:annotation>
<xs:documentation>Код вида операции</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{4}"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ДопКодОперации" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Дополнительный код вида операции</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{4}"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ПризнНеобОперации" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Код признака необычной операции</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{4}"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ПризнВалОперации" minOccurs="0">
<xs:annotation>
<xs:documentation>
Признак валютной операции.!!! Показатель введен, начиная с версии 2.0 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="VO"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ИдентификаторПД" type="ИдентификаторЗаписи" minOccurs="0">
<xs:annotation>
<xs:documentation>
Идентификатор подозрительной деятельности.!!! Показатель введен, начиная с версии 2.0 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведЦП" type="ЦифровыеПрава" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Сведения о цифровых правах</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="КодВал" type="КодВалТип" minOccurs="0">
<xs:annotation>
<xs:documentation>Код валюты</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СумОперации" type="СуммаТип" minOccurs="0">
<xs:annotation>
<xs:documentation>Сумма операции в валюте ее проведения</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СумРуб" type="СуммаТип" minOccurs="0">
<xs:annotation>
<xs:documentation>Сумма операции в рублевом эквиваленте</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="КодВалКонверсия" type="КодВалТип" minOccurs="0">
<xs:annotation>
<xs:documentation>Код продаваемой валюты</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СумКонверсия" type="СуммаТип" minOccurs="0">
<xs:annotation>
<xs:documentation>Сумма продаваемой валюты</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведДрагМеталл" type="ДрагМеталл" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Сведения о драгоценных металлах, драгоценных камнях, ювелирных изделиях и ломе из таких изделий
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ОснованиеОп" type="Документ" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Основание совершения операции</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="НазначениеПлатежа" type="Т2000П">
<xs:annotation>
<xs:documentation>Назначение платежа</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ХарактерОп" type="Т2000П">
<xs:annotation>
<xs:documentation>Характеристика операции</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="УчастникОп" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Сведения об участнике операции</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="СтатусУчастника">
<xs:annotation>
<xs:documentation>Код статуса участника операции (сделки)</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[1-4]|6|8"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ТипУчастника">
<xs:annotation>
<xs:documentation>Тип участника операции</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-5]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ПризнУчастника">
<xs:annotation>
<xs:documentation>Признак резидента (нерезидента) участника операции</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="0|1|9"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="IDППЦР" type="IDППЦРТип" minOccurs="0">
<xs:annotation>
<xs:documentation>
Уникальный идентификатор пользователя платформы цифрового рубля
!!! Показатель введен, начиная с версии 2.2 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ПризнКлиент">
<xs:annotation>
<xs:documentation>Признак участника операции (сделки)</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-2]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="КодЮЛФЛ" minOccurs="0">
<xs:annotation>
<xs:documentation>
Идентификатор участника операции, включенного в Перечень, Решение, и (или) Перечень ФРОМУ
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{1,10}"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="УчастникЮЛ" minOccurs="0">
<xs:annotation>
<xs:documentation>
Сведения о юридическом лице (филиале юридического лица, представительстве) и его единоличном исполнительном органе
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="СведЮЛ" type="СведенияЮЛ">
<xs:annotation>
<xs:documentation>
Сведения о юридическом лице (филиале (представительстве) юридического лица)
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ПризнакИдентификацияЮЛ" minOccurs="0">
<xs:annotation>
<xs:documentation>
Признак идентификации бенефициарного владельца.
!!! Показатель введен, начиная с версии 2.0 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="0|1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="БенефициарЮЛ" type="СведенияБенефициар" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Сведения о бенефициарном владельце. !!! Показатель введен, начиная с версии 2.0 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведЕИО" type="СведенияЕИО" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Сведения о единоличном исполнительном органе юридического лица нерезидента
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="УчастникФЛИП" minOccurs="0">
<xs:annotation>
<xs:documentation>
Сведения о физическом лице, индивидуальном предпринимателе, физическом лице,
занимающемся в установленном законодательством Российской Федерации порядке частной практикой
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="ИдентификацияФЛ" minOccurs="0">
<xs:annotation>
<xs:documentation>Признак идентификации физического лица</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="1|2|3"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="СведФЛИП" type="СведенияФЛИП">
<xs:annotation>
<xs:documentation>Сведения о физическом лице, индивидуальном предпринимателе</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="БенефициарФЛИП" type="СведенияБенефициар" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Сведения о бенефициарном владельце. !!! Показатель введен, начиная с версии 2.0 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="УчастникИНБОЮЛ" minOccurs="0">
<xs:annotation>
<xs:documentation>
Сведения об иностранной структуре без образования юридического лица
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="СведИНБОЮЛ" type="СведенияИНБОЮЛ">
<xs:annotation>
<xs:documentation>Сведения об иностранной структуре без образования юридического лица</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ПризнакИдентификацияИНБОЮЛ" minOccurs="0">
<xs:annotation>
<xs:documentation>
Признак идентификации бенефициарного владельца.
!!! Показатель введен, начиная с версии 2.0 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="0|1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="БенефициарИНБОЮЛ" type="СведенияБенефициар" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Сведения о бенефициарном владельце. !!! Показатель введен, начиная с версии 2.0 формата ФЭС !!!
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="КомментУчастник" type="Т2000" minOccurs="0">
<xs:annotation>
<xs:documentation>Дополнительная информация об участнике операции (сделки)</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="СведенияПереводыДС" minOccurs="0">
<xs:annotation>
<xs:documentation>
Сведения о переводах денежных средств, в том числе электронных денежных средств
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="ВидПереводДС">
<xs:annotation>
<xs:documentation>Вид перевода денежных средств</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[1-9]|1[0-4]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="КодТерИнГос" minOccurs="0" type="Т500">
<xs:annotation>
<xs:documentation>Код территории</xs:documentation>
<xs:documentation>
!!!!! Показатель добавлен в версии формата 1.2 в соответствии с Федеральным законом от 28.06.2021 № 230-ФЗ !!!!!
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ТипОператорДС">
<xs:annotation>
<xs:documentation>Тип оператора по переводу денежных средств</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[1-5]"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="НомерСчетПлательщик" type="Т40" minOccurs="0">
<xs:annotation>
<xs:documentation>Номер счета плательщика</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ИдентЭСППлательщик" type="Т100" minOccurs="0">
<xs:annotation>
<xs:documentation>Идентификатор ЭСП плательщика</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведБанкПлательщик" type="БанкПлательщикПолучатель" minOccurs="0">
<xs:annotation>
<xs:documentation>Сведения о банке плательщика</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведБанкПолучатель" type="БанкПлательщикПолучатель" minOccurs="0">
<xs:annotation>
<xs:documentation>Сведения о банке получателя</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СчетБанкПлательщик" type="Т40" minOccurs="0">
<xs:annotation>
<xs:documentation>Номер счета банка плательщика</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СчетБанкПолучатель" type="Т40" minOccurs="0">
<xs:annotation>
<xs:documentation>Номер счета банка получателя</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="НомерСчетПолучатель" type="Т40" minOccurs="0">
<xs:annotation>
<xs:documentation>Номер счета получателя</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ИдентЭСППолучателя" type="Т100" minOccurs="0">
<xs:annotation>
<xs:documentation>Идентификатор ЭСП получателя</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведПриемНалДС" type="МестоПриемаВыдача" minOccurs="0">
<xs:annotation>
<xs:documentation>Сведения о месте приема наличных денежных средств</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СтатусПеревод" minOccurs="0">
<xs:annotation>
<xs:documentation>Статус перевода</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="0|1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="СведВыдачаНалДС" type="МестоПриемаВыдача" minOccurs="0">
<xs:annotation>
<xs:documentation>Сведения о месте выдачи наличных денежных средств</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="АдресIP" type="Т40" minOccurs="0">
<xs:annotation>
<xs:documentation>IP-адрес сетевого оборудования плательщика</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="АдресMAC" type="Т23" minOccurs="0">
<xs:annotation>
<xs:documentation>MAC-адрес сетевого оборудования плательщика</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="СведенияВнесениеПолучениеНалДС" minOccurs="0">
<xs:annotation>
<xs:documentation>
Сведения о переводах денежных средств, в том числе электронных денежных средств
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="КодХарактерОп">
<xs:annotation>
<xs:documentation>Код характера операции</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="1|2"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="НомерСчетКлиент" type="Т40">
<xs:annotation>
<xs:documentation>Номер счета клиента</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="НомерКарта" type="Т40" minOccurs="0">
<xs:annotation>
<xs:documentation>Номер платежной карты</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведПриемВыдача" type="МестоПриемаВыдача" minOccurs="0">
<xs:annotation>
<xs:documentation>Сведения о месте приема (выдачи) наличных денежных средств</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="СведенияКартаИнБанк" minOccurs="0">
<xs:annotation>
<xs:documentation>
Сведения об операции с использованием платежной карты, эмитированной иностранным банком,
зарегистрированным на территории иностранного государства или административно-территориальной
единицы иностранного государства, обладающей самостоятельной правоспособностью, входящих в перечень,
утвержденный уполномоченным органом (Росфинмониторингом)
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="КодТерИнБанк" type="Т500">
<xs:annotation>
<xs:documentation>Код территории</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведМестоОперация" type="МестоПриемаВыдача">
<xs:annotation>
<xs:documentation>Сведения о месте совершения операции с денежными средствами в наличной форме</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="НомерКарта" type="Т40">
<xs:annotation>
<xs:documentation>Номер платежной карты</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СведДержательКарты" type="Т2000">
<xs:annotation>
<xs:documentation>Сведения о держателе платежной карты</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ПризнакСотрудник">
<xs:annotation>
<xs:documentation>Признак совершения операции с участием уполномоченного сотрудника кредитной организации (филиала кредитной организации)</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="0|1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="НаимИнБанк" type="Т500">
<xs:annotation>
<xs:documentation>Наименование иностранного банка</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="СВИФТИнБанк" minOccurs="0">
<xs:annotation>
<xs:documentation>СВИФТ иностранного банка</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="([A-Z]{4}[A-Z]{2}[0-9A-Z]{2}([0-9A-Z]{3}|))|НР"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Коммент" type="Т2000" minOccurs="0">
<xs:annotation>
<xs:documentation>Дополнительные сведения</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

1440
docs/xsd/data_types_v2.xsd Normal file

File diff suppressed because it is too large Load Diff

View File

@ -775,6 +775,80 @@ def test_eio_block_is_empty_when_eio_and_beneficiary_are_absent() -> None:
assert row[index] == ""
def test_participant_and_beneficiary_inn_are_scoped_regardless_of_field_order() -> None:
own_inn = ("УчастникЮЛ.СведЮЛ.ИННЮЛ", "2820000210")
beneficiary_fields = (
("УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ФИОФЛИП.Фам", "Иванов"),
("УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ФИОФЛИП.Имя", "Иван"),
("УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ФИОФЛИП.Отч", "Иванович"),
("УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП", "111111111111"),
)
for items in (
(own_inn, *beneficiary_fields),
(*reversed(beneficiary_fields), own_inn),
):
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={},
participant_fields=dict(items),
)
assert row[120] == "2820000210"
assert row[157] == "Иванов Иван Иванович"
assert row[162] == "111111111111"
assert "111111111111" not in row[157]
def test_flip_beneficiary_is_scoped_to_eio_columns() -> None:
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={},
participant_fields={
"УчастникФЛИП.СведФЛИП.ИННФЛИП": "222222222222",
"УчастникФЛИП.БенефициарФЛИП.ФЛБенефициар.ИННФЛИП": "111111111111",
},
)
assert row[120] == "222222222222"
assert row[162] == "111111111111"
def test_legal_entity_eio_name_is_preserved_in_structured_column() -> None:
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={},
participant_fields={
"УчастникЮЛ.СведЕИО.ЮЛЕИО.НаимЮЛ": "ООО Управляющая компания",
"УчастникЮЛ.СведЕИО.ЮЛЕИО.ИННЮЛ": "7707083893",
},
)
assert row[157] == "ООО Управляющая компания"
assert row[162] == "7707083893"
def test_continuation_row_validates_local_codes_without_filling_defaults() -> None:
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ПризнФТр": "9"},
participant_fields={},
is_continuation=True,
)
assert row[10] == ""
assert row[44] == ""
assert row[45] == ""
def test_cp_block_is_empty_without_sved_cp_and_uses_emitent_inn_when_present() -> None:
row_without_cp = build_fixed_row_by_index(
file_name="f.xml",

View File

@ -1,6 +1,8 @@
from __future__ import annotations
from app.pipeline.parser import parse_xml_content
import xml.etree.ElementTree as ET
from app.pipeline.parser import _extract_direct_fields, parse_xml_content
def _base_xml(operations: str) -> str:
@ -120,3 +122,105 @@ def test_parse_xml_collects_legal_entity_registration_address_attributes() -> No
assert (
participant_fields["УчастникЮЛ.СведЮЛ.АдрРегЮЛ.Район"] == "Благовещенский р"
)
def test_extract_direct_fields_indexes_repeated_siblings_at_every_level() -> None:
participant = ET.fromstring(
"""
<УчастникОп>
<Документ><НомДок>DOC-1</НомДок></Документ>
<Документ><НомДок>DOC-2</НомДок></Документ>
<УчастникЮЛ>
<СведЕИО><ФЛЕИО><ИННФЛИП>111111111111</ИННФЛИП></ФЛЕИО></СведЕИО>
<СведЕИО><ФЛЕИО><ИННФЛИП>222222222222</ИННФЛИП></ФЛЕИО></СведЕИО>
</УчастникЮЛ>
</УчастникОп>
"""
)
fields = _extract_direct_fields(participant)
assert fields["Документ[0].НомДок"] == "DOC-1"
assert fields["Документ[1].НомДок"] == "DOC-2"
assert fields["УчастникЮЛ.СведЕИО[0].ФЛЕИО.ИННФЛИП"] == "111111111111"
assert fields["УчастникЮЛ.СведЕИО[1].ФЛЕИО.ИННФЛИП"] == "222222222222"
assert all("; " not in value for value in fields.values())
def test_extract_direct_fields_keeps_single_sibling_path_unindexed() -> None:
participant = ET.fromstring(
"<УчастникОп><Документ><НомДок>DOC-1</НомДок></Документ></УчастникОп>"
)
fields = _extract_direct_fields(participant)
assert fields == {"Документ.НомДок": "DOC-1"}
def test_parse_xml_expands_documents_and_eio_into_additive_rows() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP6</ИдентификаторЗаписи>
<УчастникОп>
<ТипУчастника>1</ТипУчастника>
<Документ><НомДок>DOC-1</НомДок></Документ>
<Документ><НомДок>DOC-2</НомДок></Документ>
<Документ><НомДок>DOC-3</НомДок></Документ>
<УчастникЮЛ>
<СведЕИО><ФЛЕИО><ИННФЛИП>111111111111</ИННФЛИП></ФЛЕИО></СведЕИО>
<СведЕИО><ЮЛЕИО><ИННЮЛ>2222222222</ИННЮЛ></ЮЛЕИО></СведЕИО>
</УчастникЮЛ>
</УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert not result.is_fatal
assert len(result.rows) == 4
base_row, second_document, third_document, second_eio = result.rows
assert base_row.participant_fields["ТипУчастника"] == "1"
assert base_row.participant_fields["Документ.НомДок"] == "DOC-1"
assert (
base_row.participant_fields["УчастникЮЛ.СведЕИО.ФЛЕИО.ИННФЛИП"]
== "111111111111"
)
assert second_document.operation_fields == {}
assert second_document.is_continuation
assert second_document.participant_fields == {"Документ.НомДок": "DOC-2"}
assert third_document.participant_fields == {"Документ.НомДок": "DOC-3"}
assert second_eio.participant_fields == {
"УчастникЮЛ.СведЕИО.ЮЛЕИО.ИННЮЛ": "2222222222"
}
def test_parse_xml_separates_single_eio_and_single_beneficiary() -> None:
xml = _base_xml(
"""
<Операция>
<ИдентификаторЗаписи>OP7</ИдентификаторЗаписи>
<УчастникОп>
<ТипУчастника>1</ТипУчастника>
<УчастникЮЛ>
<СведЕИО><ЮЛЕИО><ИННЮЛ>2222222222</ИННЮЛ></ЮЛЕИО></СведЕИО>
<БенефициарЮЛ>
<ФЛБенефициар><ИННФЛИП>111111111111</ИННФЛИП></ФЛБенефициар>
</БенефициарЮЛ>
</УчастникЮЛ>
</УчастникОп>
</Операция>
"""
)
result = parse_xml_content("SKO115FZ_01_123456789_20260616_X00001.xml", xml)
assert len(result.rows) == 2
assert (
result.rows[0].participant_fields["УчастникЮЛ.СведЕИО.ЮЛЕИО.ИННЮЛ"]
== "2222222222"
)
assert result.rows[1].participant_fields == {
"УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП": "111111111111"
}

View File

@ -302,6 +302,46 @@ def test_write_report_does_not_assume_kpp_when_participant_type_is_missing(
assert sheet.cell(row=3, column=122).value == "1"
def test_write_report_keeps_only_repeat_fields_on_continuation_row(
tmp_path: Path,
) -> None:
destination = tmp_path / "report.xlsx"
rows = [
ReportRow(
file_name="f.xml",
record_id="RID",
operation_index=1,
operation_fields={"КодОперации": "5007"},
participant_fields={
"ТипУчастника": "1",
"УчастникЮЛ.СведЮЛ.ИННЮЛ": "2820000210",
},
),
ReportRow(
file_name="f.xml",
record_id="RID",
operation_index=1,
operation_fields={},
participant_fields={
"УчастникЮЛ.БенефициарЮЛ.ФЛБенефициар.ИННФЛИП": "111111111111"
},
is_continuation=True,
),
]
write_report(rows, destination)
workbook = load_workbook(destination)
sheet = workbook["Отчет"]
assert sheet.cell(row=3, column=19).value == "5007"
assert sheet.cell(row=3, column=120).value == "2820000210"
assert sheet.cell(row=4, column=19).value is None
assert sheet.cell(row=4, column=120).value in (None, "")
assert sheet.cell(row=4, column=162).value == "111111111111"
assert sheet.cell(row=4, column=44).value in (None, "")
assert sheet.cell(row=4, column=45).value in (None, "")
def _column_letter(index: int) -> str:
value = index
result = ""