Stop dropping XML values in the transfer and cash blocks

The customer's ruling of 24.09.2026 is that every value present in the XML
must reach the report, taking into account which block holds it. Two defects
broke that for operation code 5016 with transfer types 4 and 5.

Wrong paths in mapping_table.json. Columns 48-63 spelled the block as
"СведенияПереводыДс" and columns 49-59 spelled the address as
"АдрМестоПриемаВыдача"; the schema uses "СведенияПереводыДС" and
"АдрМестаПриемаВыдача". Those columns carry allow_short_lookup=false, so they
can only match on a fully scoped path and never matched at all: the whole cash
receipt block (AV-BI) and the transfer status (BJ) stayed empty on every file.
The existing test passed only because it fed the same misspellings.

Rules that deleted data instead of filling it:
- Rules 38, 44-45 and 46 wiped the account columns for code 5016. The XML of
  those records carries СчетБанкПолучатель = 00000000000000000000, which is
  exactly the value the customer asked for. The rules now only substitute the
  placeholder into an empty column, and never for 5016.
- Rule 44 also wiped the correspondent accounts for operator types 3 and 5
  even when the XML had them.
- The cashless rule wiped columns 50, 61, 65, 76, 84, 95, 99 and 101 for
  transfer types 1-4, 7 and 10-14, although type 4 is by definition a transfer
  that accepts cash and must carry the receipt block.
- Rule 62 limited the transfer status to types 2, 5 and 8.
- Rule 30 limited the metal name to code C99, while the available exports only
  ever carry code A90.

Verified on the sample exports: all 16 records with code 5016 now carry the
zeros in column 45, the real account in column 46, and no invented
placeholders in columns 38 and 44.
This commit is contained in:
Raykov-MS 2026-09-24 14:35:37 +03:00
parent 5825ca2aed
commit 880fa5811d
4 changed files with 200 additions and 100 deletions

View File

@ -24,8 +24,6 @@ COL_SALE_CURRENCY = _index(25)
COL_SALE_AMOUNT = _index(26)
COL_CURRENCY_SIGN = _index(27)
COL_SUSPICIOUS_ID = _index(28)
COL_METAL_CODE = _index(29)
COL_METAL_NAME = _index(30)
COL_ITEM_TYPE = _index(31)
COL_EXTRA_INFO = _index(34)
@ -43,7 +41,6 @@ COL_PAYER_BANK_ACCOUNT = _index(44)
COL_RECIPIENT_BANK_ACCOUNT = _index(45)
COL_RECIPIENT_ACCOUNT = _index(46)
COL_RECIPIENT_ESP = _index(47)
COL_TRANSFER_STATUS = _index(62)
# Карты и место операции.
COL_TERRITORY_CODE_CARD = _index(96)
@ -87,11 +84,6 @@ PARTICIPANT_ADDRESS_COMPONENT_COLUMNS = tuple(
if column.structured_group == "legacy_142" and column.structured_role == "component"
)
# Точные поля наличных, которые исторически очищаются для безналичных переводов.
CASHLESS_CLEARED_COLUMNS = tuple(
_index(position) for position in (50, 61, 65, 76, 84, 95, 99, 101)
)
OKATO_COUNTRY_PAIRS: tuple[tuple[int, int], ...] = tuple(
(_index(country), _index(okato))
for country, okato in (
@ -106,16 +98,12 @@ OKATO_COUNTRY_PAIRS: tuple[tuple[int, int], ...] = tuple(
# Бизнес-константы.
ACCOUNT_PLACEHOLDER = "00000000000000000000"
EXPORT_SUBSIDIARY_COMMENT_CODES = frozenset(str(code) for code in range(6102, 6127))
CASHLESS_TRANSFER_TYPES = frozenset(
{"1", "2", "3", "4", "7", "10", "11", "12", "13", "14"}
)
VALID_FTR_SIGNS = frozenset({"0", "1", "2", "3", "4", "5"})
VALID_SUSPENSION_BASES = frozenset({"1", "2", "3"})
VALID_ESP_SIGNS = frozenset({"1", "2", "3", "4"})
PRECIOUS_ITEM_CODES = frozenset({"5020", "5021", "5022", "5023"})
SUSPICIOUS_CODE = "6001"
TERRITORY_CODE = "5016"
PRECIOUS_METAL_CODE = "C99"
EMPLOYEE_ABSENT_CODE = "0"
RUSSIA_COUNTRY_CODE = "643"

View File

@ -7,8 +7,6 @@ from .column_constants import (
ACCOUNT_PLACEHOLDER,
BLOCK_CP,
BLOCK_EIO,
CASHLESS_CLEARED_COLUMNS,
CASHLESS_TRANSFER_TYPES,
COL_ADDRESS_ONE_LINE,
COL_ADDRESS_STRUCTURED,
COL_AMOUNT_CURRENCY,
@ -38,8 +36,6 @@ from .column_constants import (
COL_INSURANCE_NUMBER,
COL_ITEM_TYPE,
COL_KPP,
COL_METAL_CODE,
COL_METAL_NAME,
COL_OPERATION_CODE,
COL_OPERATION_DOCUMENTS,
COL_OPERATION_SIGN,
@ -65,7 +61,6 @@ from .column_constants import (
COL_SUSPENSION_BASIS,
COL_SUSPICIOUS_ID,
COL_TERRITORY_CODE,
COL_TRANSFER_STATUS,
COL_TRANSFER_TYPE,
COL_UNUSUAL_CODES,
EMPLOYEE_ABSENT_CODE,
@ -77,7 +72,6 @@ from .column_constants import (
PARTICIPANT_TYPE_IP,
PARTICIPANT_TYPE_UL,
PRECIOUS_ITEM_CODES,
PRECIOUS_METAL_CODE,
RUSSIA_COUNTRY_CODE,
SUSPICIOUS_CODE,
TERRITORY_CODE,
@ -886,7 +880,6 @@ def _apply_business_rules(
_rule_sale_currency(row)
_rule_extra_info(row, codes, operation_lookup, participant_lookup)
_rule_suspicious_activity(row, codes)
_rule_metal_name(row)
_rule_item_type(row, codes)
_rule_territory_codes(row, codes)
@ -895,10 +888,8 @@ def _apply_business_rules(
_rule_payer_bank(row, operator_type)
_rule_recipient_bank(row, operator_type)
_rule_correspondent_accounts(row, operator_type, transfer_type, codes)
_rule_recipient_account(row, transfer_type)
_rule_transfer_status(row, transfer_type)
_rule_recipient_account(row, transfer_type, codes)
_rule_cashless_clears_cash_columns(row, transfer_type)
_rule_card_without_employee(row)
_rule_participant_identity(row, participant_type)
@ -991,12 +982,6 @@ def _rule_suspicious_activity(row: dict[int, str], codes: set[str]) -> None:
_set(row, COL_SUSPICIOUS_ID, "")
def _rule_metal_name(row: dict[int, str]) -> None:
"""Правило 30: наименование металла только для кода C99."""
if _get(row, COL_METAL_CODE).upper() != PRECIOUS_METAL_CODE:
_set(row, COL_METAL_NAME, "")
def _rule_item_type(row: dict[int, str], codes: set[str]) -> None:
"""Правило 31: предмет операции только для кодов 5020-5023."""
if not codes & PRECIOUS_ITEM_CODES:
@ -1022,19 +1007,17 @@ def _rule_territory_codes(row: dict[int, str], codes: set[str]) -> None:
def _rule_payer_account(
row: dict[int, str], transfer_type: str, codes: set[str]
) -> None:
"""Правило 38: счёт плательщика зависит от вида перевода.
"""Правило 38: заглушка счёта плательщика для отдельных видов перевода.
Для операций с кодом 5016 (п.6 правок) заглушку не подставляем — такого
блока (номера счетов) для 5016 в спецификации нет.
Значение, пришедшее из XML, не затирается никогда: заказчик 24.09.2026
подтвердил, что в отчёт должны попадать все значения из файла. Правило
только подставляет заглушку в пустую колонку, а для операций с кодом 5016
(п.6 правок) не делает и этого — своего блока номеров счетов у 5016 нет.
"""
if TERRITORY_CODE in codes:
_set(row, COL_PAYER_ACCOUNT, "")
return
if transfer_type in {"1", "2", "3", "10"}:
if not _get(row, COL_PAYER_ACCOUNT):
_set(row, COL_PAYER_ACCOUNT, ACCOUNT_PLACEHOLDER)
elif transfer_type not in {"12", "13", "14"}:
_set(row, COL_PAYER_ACCOUNT, "")
if transfer_type in {"1", "2", "3", "10"} and not _get(row, COL_PAYER_ACCOUNT):
_set(row, COL_PAYER_ACCOUNT, ACCOUNT_PLACEHOLDER)
def _rule_esp_identifiers(row: dict[int, str], operation_sign: str) -> None:
@ -1060,51 +1043,46 @@ def _rule_recipient_bank(row: dict[int, str], operator_type: str) -> None:
def _rule_correspondent_accounts(
row: dict[int, str], operator_type: str, transfer_type: str, codes: set[str]
) -> None:
"""Правила 44-45: корсчета зависят от оператора и вида перевода.
"""Правила 44-45: заглушки корсчетов зависят от оператора и вида перевода.
Для операций с кодом 5016 (п.6 правок) заглушку не подставляем — такого
блока (номера счетов) для 5016 в спецификации нет.
Как и в правиле 38, пришедшее из XML значение сохраняется всегда. Это
важно именно для кода 5016: в таких записях в XML реально лежит
СчетБанкПолучатель = 00000000000000000000, и заказчик 24.09.2026 отдельно
указал, что эти нули обязаны попадать в отчёт. Раньше их затирала ветка
по коду 5016, а корсчёт плательщика — ветка по типам оператора 3 и 5.
"""
if TERRITORY_CODE in codes:
_set(row, COL_PAYER_BANK_ACCOUNT, "")
_set(row, COL_RECIPIENT_BANK_ACCOUNT, "")
return
if not operator_type and not transfer_type:
_set(row, COL_PAYER_BANK_ACCOUNT, "")
_set(row, COL_RECIPIENT_BANK_ACCOUNT, "")
return
if operator_type in {"3", "5"} or transfer_type in {"12", "13"}:
_set(row, COL_PAYER_BANK_ACCOUNT, "")
elif not _get(row, COL_PAYER_BANK_ACCOUNT):
if (
operator_type not in {"3", "5"}
and transfer_type not in {"12", "13"}
and not _get(row, COL_PAYER_BANK_ACCOUNT)
):
_set(row, COL_PAYER_BANK_ACCOUNT, ACCOUNT_PLACEHOLDER)
if operator_type in {"3", "5"} or transfer_type in {"10", "11"}:
_set(row, COL_RECIPIENT_BANK_ACCOUNT, "")
elif not _get(row, COL_RECIPIENT_BANK_ACCOUNT):
if (
operator_type not in {"3", "5"}
and transfer_type not in {"10", "11"}
and not _get(row, COL_RECIPIENT_BANK_ACCOUNT)
):
_set(row, COL_RECIPIENT_BANK_ACCOUNT, ACCOUNT_PLACEHOLDER)
def _rule_recipient_account(row: dict[int, str], transfer_type: str) -> None:
"""Правило 46: счёт получателя зависит от вида перевода."""
if transfer_type in {"1", "4", "7", "12"}:
if not _get(row, COL_RECIPIENT_ACCOUNT):
_set(row, COL_RECIPIENT_ACCOUNT, ACCOUNT_PLACEHOLDER)
elif transfer_type not in {"10", "11", "14"}:
_set(row, COL_RECIPIENT_ACCOUNT, "")
def _rule_recipient_account(
row: dict[int, str], transfer_type: str, codes: set[str]
) -> None:
"""Правило 46: заглушка счёта получателя для отдельных видов перевода.
def _rule_transfer_status(row: dict[int, str], transfer_type: str) -> None:
"""Правило 62: статус перевода только для видов 2, 5 и 8."""
if transfer_type not in {"2", "5", "8"}:
_set(row, COL_TRANSFER_STATUS, "")
def _rule_cashless_clears_cash_columns(row: dict[int, str], transfer_type: str) -> None:
"""Для безналичных переводов очищаем согласованные наличные поля."""
if transfer_type in CASHLESS_TRANSFER_TYPES:
_clear_columns(row, CASHLESS_CLEARED_COLUMNS)
Как и правила 38 и 44-45, значение из XML не затирает, а для кода 5016
заглушку не подставляет.
"""
if TERRITORY_CODE in codes:
return
if transfer_type in {"1", "4", "7", "12"} and not _get(row, COL_RECIPIENT_ACCOUNT):
_set(row, COL_RECIPIENT_ACCOUNT, ACCOUNT_PLACEHOLDER)
def _rule_card_without_employee(row: dict[int, str]) -> None:

View File

@ -946,7 +946,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "ИдТерминал",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/ИдТерминал",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/ИдТерминал",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -966,7 +966,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/АдресСтрока",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/АдресСтрока",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -986,7 +986,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/АдресСтрока",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/АдресСтрока",
"source_scope": "any",
"allow_short_lookup": true,
"allow_direct_mapping": true,
@ -1006,7 +1006,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/Индекс",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/Индекс",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1026,7 +1026,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/КодОКСМ",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/КодОКСМ",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1046,7 +1046,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/КодСубъектаПоОКАТО",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/КодСубъектаПоОКАТО",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1066,7 +1066,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/Район",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/Район",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1086,7 +1086,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/Пункт",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/Пункт",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1106,7 +1106,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/Улица",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/Улица",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1126,7 +1126,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/Дом",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/Дом",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1146,7 +1146,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/Корп",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/Корп",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1166,7 +1166,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "АдрМестаПриемаВыдача",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/АдрМестоПриемаВыдача/Оф",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/АдрМестаПриемаВыдача/Оф",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1186,7 +1186,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "НаимКО",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/НаимКО",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/НаимКО",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1206,7 +1206,7 @@
"block": "cash_receipt",
"report_group": "cash_receipt",
"xml_tag": "БИККО",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведПриемНалДС/БИККО",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведПриемНалДС/БИККО",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1226,7 +1226,7 @@
"block": "transfer_status",
"report_group": "transfer_status",
"xml_tag": "СтатусПеревод",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СтатусПеревод",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СтатусПеревод",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,
@ -1246,7 +1246,7 @@
"block": "cash_payment",
"report_group": "cash_payment",
"xml_tag": "ИдТерминал",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДс/СведВыдачаНалДС/ИдТерминал",
"xml_path": "/СообщОперКО/ИнформЧасть/СведКО/Операция/СведенияПереводыДС/СведВыдачаНалДС/ИдТерминал",
"source_scope": "any",
"allow_short_lookup": false,
"allow_direct_mapping": true,

View File

@ -437,6 +437,23 @@ def test_operator_type_conditions_clear_irrelevant_bank_fields() -> None:
assert row["Наименование банка плательщика"] == ""
assert row["Наименование банка получателя"] == ""
assert row["БИК банка получателя"] == ""
# Корсчета при типе оператора 3 заглушкой не заполняются, но и не
# затираются: заказчик 24.09.2026 потребовал выводить всё, что пришло
# в XML. Раньше эти два значения удалялись.
assert row["Номер счета банка плательщика"] == "30101810000000000001"
assert row["Номер счета банка получателя"] == "30101810000000000002"
def test_correspondent_accounts_stay_empty_for_operator_3_without_xml_values() -> None:
"""Обратная сторона предыдущего теста: если в XML корсчетов нет, при типе
оператора 3 заглушка по-прежнему не подставляется."""
row = build_fixed_row(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ТипОператорДС": "3", "ВидПереводДС": "1"},
participant_fields={},
)
assert row["Номер счета банка плательщика"] == ""
assert row["Номер счета банка получателя"] == ""
@ -877,25 +894,33 @@ def test_operation_documents_keep_positions_and_join_multiple_documents() -> Non
def test_structured_cash_address_does_not_include_bank_fields() -> None:
"""Ключи здесь записаны так, как их отдаёт парсер по схеме
SKO115FZ_OPER_v2.xsd: блок называется "СведенияПереводыДС" (обе последние
буквы заглавные), а адрес — "АдрМестаПриемаВыдача". Раньше и тест, и
mapping_table.json содержали одни и те же опечатки ("СведенияПереводыДс",
"АдрМестоПриемаВыдача"), поэтому тест проходил, а на реальных файлах весь
блок приёма наличных оставался пустым."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"СведенияПереводыДс.СведПриемНалДС.АдрМестоПриемаВыдача.Индекс": "675520",
"СведенияПереводыДс.СведПриемНалДС.АдрМестоПриемаВыдача.КодОКСМ": "643",
"СведенияПереводыДс.СведПриемНалДС.АдрМестоПриемаВыдача.КодСубъектаПоОКАТО": "10",
"СведенияПереводыДс.СведПриемНалДС.АдрМестоПриемаВыдача.Пункт": "Чигири с",
"СведенияПереводыДс.СведПриемНалДС.АдрМестоПриемаВыдача.Улица": "Зеленая ул",
"СведенияПереводыДс.СведПриемНалДС.АдрМестоПриемаВыдача.Дом": "1",
"СведенияПереводыДс.СведПриемНалДС.БИККО": "044525111",
"СведенияПереводыДс.СведПриемНалДС.НаимКО": "АО РСХБ",
"СведенияПереводыДС.СведПриемНалДС.АдрМестаПриемаВыдача.Индекс": "675520",
"СведенияПереводыДС.СведПриемНалДС.АдрМестаПриемаВыдача.КодОКСМ": "643",
"СведенияПереводыДС.СведПриемНалДС.АдрМестаПриемаВыдача.КодСубъектаПоОКАТО": "10",
"СведенияПереводыДС.СведПриемНалДС.АдрМестаПриемаВыдача.Пункт": "Чигири с",
"СведенияПереводыДС.СведПриемНалДС.АдрМестаПриемаВыдача.Улица": "Зеленая ул",
"СведенияПереводыДС.СведПриемНалДС.АдрМестаПриемаВыдача.Дом": "1",
"СведенияПереводыДС.СведПриемНалДС.БИККО": "044525111",
"СведенияПереводыДС.СведПриемНалДС.НаимКО": "АО РСХБ",
},
participant_fields={},
)
assert row[50] == "675520, 643, 10, , Чигири с, Зеленая ул, 1, , "
assert "044525111" not in row[50]
assert "АО РСХБ" not in row[50]
assert row[61] == "044525111"
assert row[60] == "АО РСХБ"
def test_participant_structured_address_stays_in_142_and_components_143_plus() -> None:
@ -1140,21 +1165,40 @@ def test_col179_keeps_structured_eio_registration_without_one_line() -> None:
assert row[179] == "123456, 643, Москва"
def test_cash_columns_cleared_for_cashless_transfer_type_1() -> None:
def test_cash_columns_keep_xml_values_for_any_transfer_type() -> None:
"""Раньше для видов перевода 1-4, 7 и 10-14 колонки 50, 61, 65, 76, 84,
95, 99 и 101 очищались как "безналичные". Заказчик 24.09.2026 указал, что
в отчёт должны попадать все значения из XML, а для вида перевода 4 блок
приёма наличных (включая БИК банка плательщика) обязателен."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"ВидПереводДС": "1",
"КодОперации": "4006",
"БИККО": "044525111",
"СведенияПереводыДс.СведПриемНалДС.БИККО": "044525111",
"ВидПереводДС": "4",
"КодОперации": "5016",
"СведенияПереводыДС.СведПриемНалДС.БИККО": "044525111",
"СведенияПереводыДС.СведПриемНалДС.АдрМестаПриемаВыдача.Индекс": "675520",
},
participant_fields={},
)
for index in (50, 61, 65, 76, 84, 95, 99, 101):
assert row[index] == ""
assert row[61] == "044525111"
assert row[51] == "675520"
assert row[50].startswith("675520")
def test_cash_columns_stay_empty_when_xml_has_no_cash_block() -> None:
"""Проверяем, что удаление правила "безналичных" не начало выдумывать
значения: без блоков наличных колонки остаются пустыми."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={"ВидПереводДС": "1", "КодОперации": "9003"},
participant_fields={},
)
for index in (48, 50, 61, 63, 65, 76, 84, 95, 99, 101):
assert row[index] == "", index
def test_col119_is_empty_for_legal_entity_and_uses_flip_identifier() -> None:
@ -1668,6 +1712,96 @@ def test_col38_44_45_payer_accounts_blank_for_5016() -> None:
assert row[46] == "40817810106240020211" # реальные данные не трогаем
def test_col45_keeps_zero_account_from_xml_for_5016() -> None:
"""Заказчик 24.09.2026: "в эти операциях не тянутся нули в колонку счет
банка получателя". В XML операций 5016 реально лежит
СчетБанкПолучатель = 00000000000000000000, а прежняя ветка правил 44-45
затирала всю пару корсчетов по коду 5016."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"КодОперации": "5016",
"ВидПереводДС": "5",
"ТипОператорДС": "2",
"СчетБанкПолучатель": "00000000000000000000",
},
participant_fields={},
)
assert row[45] == "00000000000000000000"
# Заглушки для 5016 по-прежнему не подставляются.
assert row[38] == ""
assert row[44] == ""
assert row[46] == ""
def test_cash_receipt_and_status_filled_for_5016_transfer_type_5() -> None:
"""Заказчик 24.09.2026: при виде перевода 5 должны быть заполнены блок
приёма наличных, блок выдачи наличных и статус перевода — колонки AV-BX,
то есть 48-76, в зависимости от наличия сведений."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"КодОперации": "5016",
"ВидПереводДС": "5",
"ТипОператорДС": "2",
"СведенияПереводыДС.СведПриемНалДС.ИдТерминал": "ATM-111",
"СведенияПереводыДС.СведПриемНалДС.НаимКО": "Банк приёма",
"СведенияПереводыДС.СведПриемНалДС.БИККО": "040813733",
"СведенияПереводыДС.СтатусПеревод": "1",
"СведенияПереводыДС.СведВыдачаНалДС.ИдТерминал": "ATM-222",
"СведенияПереводыДС.СведВыдачаНалДС.НаимКО": "Банк выдачи",
"СведенияПереводыДС.СведВыдачаНалДС.БИККО": "041012731",
},
participant_fields={},
)
assert row[48] == "ATM-111"
assert row[60] == "Банк приёма"
assert row[61] == "040813733"
assert row[62] == "1"
assert row[63] == "ATM-222"
assert row[75] == "Банк выдачи"
assert row[76] == "041012731"
def test_transfer_status_is_not_limited_to_transfer_types_2_5_8() -> None:
"""Прежнее правило 62 выводило статус только для видов перевода 2, 5 и 8.
По общему правилу заказчика статус выводится всегда, когда тег есть
в XML."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"ВидПереводДС": "1",
"СведенияПереводыДС.СтатусПеревод": "0",
},
participant_fields={},
)
assert row[62] == "0"
def test_metal_name_is_not_limited_to_code_c99() -> None:
"""Прежнее правило 30 выводило наименование металла только при коде C99.
В доступных выгрузках встречается код A90, и по общему правилу заказчика
наименование должно попадать в отчёт при любом коде."""
row = build_fixed_row_by_index(
file_name="f.xml",
record_id="R1",
operation_index=1,
operation_fields={
"СведДрагМеталл.КодДрагМеталл": "A90",
"СведДрагМеталл.НаимДрагМеталл": "Золото",
},
participant_fields={},
)
assert row[29] == "A90"
assert row[30] == "Золото"
def test_col38_44_payer_accounts_still_get_placeholder_without_5016() -> None:
"""Убеждаемся, что правка п.6 не задела остальные виды перевода —
заглушка по-прежнему подставляется, если 5016 нет в кодах операции."""