diff --git a/app/pipeline/column_constants.py b/app/pipeline/column_constants.py index 6049b06..6f299c8 100644 --- a/app/pipeline/column_constants.py +++ b/app/pipeline/column_constants.py @@ -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" diff --git a/app/pipeline/mapping.py b/app/pipeline/mapping.py index 19d04da..c209a67 100644 --- a/app/pipeline/mapping.py +++ b/app/pipeline/mapping.py @@ -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: diff --git a/app/pipeline/mapping_table.json b/app/pipeline/mapping_table.json index 4c8dfbb..c4302b9 100644 --- a/app/pipeline/mapping_table.json +++ b/app/pipeline/mapping_table.json @@ -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, diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index 1d08418..0a201cb 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -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 нет в кодах операции."""