diff --git a/app/pipeline/column_constants.py b/app/pipeline/column_constants.py index 28d4998..b50d994 100644 --- a/app/pipeline/column_constants.py +++ b/app/pipeline/column_constants.py @@ -102,13 +102,6 @@ OKATO_COUNTRY_PAIRS: tuple[tuple[int, int], ...] = tuple( ) ) -# Текущая перестановка адресного блока при выгрузке в целевой шаблон. -PARTICIPANT_ADDRESS_OUTPUT_SOURCES: dict[int, int] = { - _index(141): _index(142), - **{_index(target): _index(target + 1) for target in range(142, 151)}, - _index(151): _index(141), -} - # Бизнес-константы. ACCOUNT_PLACEHOLDER = "00000000000000000000" EXPORT_SUBSIDIARY_COMMENT_CODES = frozenset(str(code) for code in range(6102, 6127)) diff --git a/app/pipeline/mapping.py b/app/pipeline/mapping.py index 585965e..f66fdb2 100644 --- a/app/pipeline/mapping.py +++ b/app/pipeline/mapping.py @@ -131,6 +131,7 @@ class StructuredGroupRule: one_line_columns: tuple[int, ...] component_columns: tuple[int, ...] join_with_space: bool = False + preserve_empty_positions: bool = False _INVALID_TAGS = {"", "-", "Источник", "путь", "подразумеваются", "тэга", "нашла"} @@ -366,6 +367,7 @@ def _build_structured_group_rules( one_line_columns=one_line_columns, component_columns=component_columns, join_with_space=aggregate.join_with_space, + preserve_empty_positions="адрес" in aggregate.name.lower(), ) ) return tuple(sorted(rules, key=lambda rule: rule.structured_column)) @@ -541,14 +543,18 @@ def _apply_structured_group_rules(row: dict[int, str]) -> None: _set(row, component_column_index, "") continue - values = [ + component_values = [ _get(row, component_column_index).strip() for component_column_index in rule.component_columns - if _get(row, component_column_index).strip() ] - if not values: + if not any(component_values): continue + values = ( + component_values + if rule.preserve_empty_positions + else [value for value in component_values if value] + ) separator = " " if rule.join_with_space else ", " _set(row, rule.structured_column, separator.join(values)) @@ -687,7 +693,7 @@ def _format_operation_documents(lookup: _PayloadLookup) -> str: document.get(field_name, "") for field_name in _OPERATION_DOCUMENT_FIELDS ] fields.insert(1, "") - formatted_documents.append(f"{position}, {', '.join(fields)};") + formatted_documents.append(f"{position}. {', '.join(fields)};") return "\n".join(formatted_documents) diff --git a/app/pipeline/report.py b/app/pipeline/report.py index 4f3458a..4348312 100644 --- a/app/pipeline/report.py +++ b/app/pipeline/report.py @@ -11,12 +11,7 @@ from tempfile import NamedTemporaryFile import xlsxwriter -from .column_constants import ( - COL_KPP, - COL_PARTICIPANT_TYPE, - PARTICIPANT_ADDRESS_OUTPUT_SOURCES, - PARTICIPANT_TYPE_UL, -) +from .column_constants import COL_KPP, COL_PARTICIPANT_TYPE, PARTICIPANT_TYPE_UL from .column_registry import COLUMNS from .mapping import FIXED_REPORT_COLUMNS, build_fixed_row_by_index @@ -136,10 +131,7 @@ class StreamingReportWriter: column_index: int, row_by_index: dict[int, str], ) -> object: - source_index = PARTICIPANT_ADDRESS_OUTPUT_SOURCES.get( - column_index, column_index - ) - return row_by_index.get(source_index, "") + return row_by_index.get(column_index, "") def save(self, destination: Path) -> None: destination.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index 642be10..dc3b879 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -117,7 +117,7 @@ def test_build_fixed_row_splits_pipe_separated_xml_tag_values() -> None: "УчастникФЛИП.СведФЛИП.АдрРег.Индекс": "674500", }, ) - assert row["Адрес по структуре (целый)"] == "674500" + assert row["Адрес по структуре (целый)"] == "674500, , , , , , , , " def test_extract_tag_candidate_keys_supports_comma_separator() -> None: @@ -201,7 +201,9 @@ def test_structured_address_is_built_from_right_columns() -> None: "УчастникФЛИП.СведФЛИП.АдрРег.Дом": "1", }, ) - assert row["Адрес по структуре (целый)"] == "674500, 643, Москва, Тверская, 1" + assert ( + row["Адрес по структуре (целый)"] == "674500, 643, , , Москва, Тверская, 1, , " + ) def test_one_line_address_clears_structured_and_component_columns() -> None: @@ -589,7 +591,7 @@ def test_foreign_structure_founder_address_populates_eio_address_block() -> None ) assert row[167] == "" - assert row[168] == "101000, 643, Москва" + assert row[168] == "101000, 643, , , Москва, , , , " assert row[169] == "101000" assert row[170] == "643" assert row[173] == "Москва" @@ -848,9 +850,9 @@ def test_operation_documents_keep_positions_and_join_multiple_documents() -> Non ) assert row[107] == ( - "1, 13, , , 29/01/2026, 253, Размещение денежных средств;\n" - "2, 99, , Депозитный договор, 29/01/2026, Р202307/0001/93/Э, ;\n" - "3, 99, , Общие условия размещения депозитов, 17/11/2020, Р202307/0001, ;" + "1. 13, , , 29/01/2026, 253, Размещение денежных средств;\n" + "2. 99, , Депозитный договор, 29/01/2026, Р202307/0001/93/Э, ;\n" + "3. 99, , Общие условия размещения депозитов, 17/11/2020, Р202307/0001, ;" ) @@ -871,7 +873,7 @@ def test_structured_cash_address_does_not_include_bank_fields() -> None: }, participant_fields={}, ) - assert row[50] == "675520, 643, 10, Чигири с, Зеленая ул, 1" + assert row[50] == "675520, 643, 10, , Чигири с, Зеленая ул, 1, , " assert "044525111" not in row[50] assert "АО РСХБ" not in row[50] @@ -892,11 +894,100 @@ def test_participant_structured_address_stays_in_142_and_components_143_plus() - }, ) assert row[141] == "" - assert row[142] == "123456, 643, 45, Москва, Тверская, 1" + assert row[142] == "123456, 643, 45, , Москва, Тверская, 1, , " assert row[143] == "123456" assert row[144] == "643" +def test_shagidova_address_keeps_missing_component_positions() -> None: + row = build_fixed_row_by_index( + file_name="SKO115FZ_01_044525111_20260810_000001.xml", + record_id="R1", + operation_index=1, + operation_fields={"НомерОперация": "ЦФТ-БАНК_1361449333033"}, + participant_fields={ + "УчастникФЛИП.СведФЛИП.АдрРег.Индекс": "367014", + "УчастникФЛИП.СведФЛИП.АдрРег.КодОКСМ": "643", + "УчастникФЛИП.СведФЛИП.АдрРег.КодСубъектаПоОКАТО": "82", + "УчастникФЛИП.СведФЛИП.АдрРег.Пункт": "Махачкала г.", + "УчастникФЛИП.СведФЛИП.АдрРег.Улица": "Гаджиева А ул", + "УчастникФЛИП.СведФЛИП.АдрРег.Дом": "12 Ж", + "УчастникФЛИП.СведФЛИП.АдрРег.Оф": "29", + }, + ) + + assert row[142] == ("367014, 643, 82, , Махачкала г., Гаджиева А ул, 12 Ж, , 29") + assert tuple(row[index] for index in range(143, 152)) == ( + "367014", + "643", + "82", + "", + "Махачкала г.", + "Гаджиева А ул", + "12 Ж", + "", + "29", + ) + + +def test_participant_full_address_keeps_all_nine_components() -> None: + components = { + "Индекс": "367014", + "КодОКСМ": "643", + "КодСубъектаПоОКАТО": "82", + "Район": "Советский р-н", + "Пункт": "Махачкала г.", + "Улица": "Гаджиева А ул", + "Дом": "12 Ж", + "Корп": "2", + "Оф": "29", + } + row = build_fixed_row_by_index( + file_name="f.xml", + record_id="R1", + operation_index=1, + operation_fields={}, + participant_fields={ + f"УчастникФЛИП.СведФЛИП.АдрРег.{tag}": value + for tag, value in components.items() + }, + ) + + assert row[142] == ", ".join(components.values()) + assert tuple(row[index] for index in range(143, 152)) == tuple(components.values()) + + +def test_eio_address_keeps_missing_component_positions() -> None: + row = build_fixed_row_by_index( + file_name="f.xml", + record_id="R1", + operation_index=1, + operation_fields={}, + participant_fields={ + "УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.Индекс": "367014", + "УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.КодОКСМ": "643", + "УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.КодСубъектаПоОКАТО": "82", + "УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.Пункт": "Махачкала г.", + "УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.Улица": "Гаджиева А ул", + "УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.Дом": "12 Ж", + "УчастникЮЛ.СведЕИО.ФЛЕИО.АдрРег.Оф": "29", + }, + ) + + assert row[168] == ("367014, 643, 82, , Махачкала г., Гаджиева А ул, 12 Ж, , 29") + assert tuple(row[index] for index in range(169, 178)) == ( + "367014", + "643", + "82", + "", + "Махачкала г.", + "Гаджиева А ул", + "12 Ж", + "", + "29", + ) + + def test_participant_address_does_not_use_citizenship_without_address_block() -> None: row = build_fixed_row_by_index( file_name="f.xml", @@ -930,7 +1021,7 @@ def test_participant_address_country_is_isolated_from_citizenship() -> None: ) assert row[126] == "762" - assert row[142] == "123456, 643, Москва" + assert row[142] == "123456, 643, , , Москва, , , , " assert row[144] == "643" diff --git a/tests/unit/test_report.py b/tests/unit/test_report.py index 7dc2abf..f667549 100644 --- a/tests/unit/test_report.py +++ b/tests/unit/test_report.py @@ -229,7 +229,7 @@ def test_write_report_restores_leading_zeros_for_bik_inn_kpp(tmp_path: Path) -> assert sheet.cell(row=3, column=kpp_col).value == "032601001" -def test_write_report_shifts_participant_address_block_to_expected_columns( +def test_write_report_preserves_participant_address_column_positions( tmp_path: Path, ) -> None: destination = tmp_path / "report.xlsx" @@ -260,12 +260,14 @@ def test_write_report_shifts_participant_address_block_to_expected_columns( workbook = load_workbook(destination) sheet = workbook["Отчет"] - assert sheet.cell(row=3, column=141).value == ( + assert sheet.cell(row=3, column=141).value == "г. Благовещенск" + assert sheet.cell(row=3, column=142).value == ( "675520, 643, 10, Благовещенский р-н, Чигири с, Зеленая ул, 1" ) - assert sheet.cell(row=3, column=142).value == "675520" - assert sheet.cell(row=3, column=143).value == "643" - assert sheet.cell(row=3, column=144).value == "10" + assert sheet.cell(row=3, column=143).value == "675520" + assert sheet.cell(row=3, column=144).value == "643" + assert sheet.cell(row=3, column=145).value == "10" + assert sheet.cell(row=3, column=146).value == "Благовещенский р-н" def test_write_report_does_not_pad_physical_person_identification_as_kpp(