"""Реквизит субконто «Контрагенты.ИНН» строкой под контрагентом (OQ-162, issue #32; `05-domain-rules.md` §1.1, §1.3). 1С с шапкой варианта 2 «субконто: Контрагенты, Контрагенты.ИНН» выводит ИНН отдельной строкой на уровень глубже контрагента, с теми же суммами и числом в ячейке. Это реквизит контрагента, а не второй уровень контрагентов: ИНН переходит к родителю, строка из `rows` уходит; не склеилось — строка OTHER и `needs_check`. """ from decimal import Decimal import pytest from gridbuilder import L, M, sheet_from_rows from src.contract.result import OsvDocument from src.parser.deadline import Deadline from src.parser.grid import Sheet from src.parser.osv import OsvParser from src.parser.rules import Rules, load_rules TIMEOUT_SECONDS = 60 DETAIL = "Детализация по субсчетам, субконто: Контрагенты, Контрагенты.ИНН" Sums = tuple[object, object, object, object, object, object] A: Sums = (None, None, 760000, 760000, None, None) B: Sums = (None, None, 100000, 104500, None, 4500) ROOT: Sums = (None, None, 860000, 864500, None, 4500) @pytest.fixture(scope="module") def rules() -> Rules: return load_rules() def _sheet(*rows: list[object], detail: str = DETAIL) -> Sheet: head: list[list[object]] = [ ["ООО Зерновая"], ["Оборотно-сальдовая ведомость по счету 60"], ["Период: 9 месяцев 2025 г."], [detail], ["Выводимые данные: сумма"], [], ["Субконто", M("Сальдо на начало периода", 2), M("Оборот за период", 2), M("Сальдо на конец периода", 2)], [None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"], ] return sheet_from_rows([*head, *rows]) def r(level: int, text: object, sums: Sums) -> list[object]: return [L(level), text, *sums] def _parse(sheet: Sheet, rules: Rules) -> tuple[OsvDocument, list[str], bool]: return OsvParser(rules).parse_sheet(sheet, Deadline(TIMEOUT_SECONDS)) def _values(document: OsvDocument) -> list[str]: return [row.row_value for row in document.rows] def test_inn_attribute_is_not_a_group_and_owner_has_inn(rules: Rules) -> None: sheet = _sheet(r(0, "60.01", A), r(1, "А2 БИЗНЕС ООО", A), r(2, 2370012036, A), [L(0), "Итого", *A]) document, _, _ = _parse(sheet, rules) assert [(g.type, g.name, g.has_inn) for g in document.group_schema] == [ ("ACCOUNT", "Субконто", None), ("COUNTERPARTY", "Контрагенты", True), ] def test_inn_row_is_glued_to_parent_counterparty(rules: Rules) -> None: sheet = _sheet( r(0, "60.01", ROOT), # 9 r(1, "А2 БИЗНЕС ООО", A), # 10 r(2, 2370012036, A), # 11 r(1, "Аббазов Игорь Рафаилович", B), # 12 r(2, Decimal("501604329883"), B), # 13 [L(0), "Итого", *ROOT], ) document, warnings, ok = _parse(sheet, rules) assert _values(document) == ["60.01", "А2 БИЗНЕС ООО", "Аббазов Игорь Рафаилович", "Итого"] counterparties = [row for row in document.rows if row.group_type == "COUNTERPARTY"] fields = [(row.counterparty_name, row.counterparty_inn, row.level, row.parent_row_number) for row in counterparties] assert fields == [ ("А2 БИЗНЕС ООО", "2370012036", 1, 9), ("Аббазов Игорь Рафаилович", "501604329883", 1, 9), ] assert ok assert any("2370012036" in warning and "строке 10" in warning for warning in warnings), warnings def test_inn_number_with_lost_leading_zero_is_restored(rules: Rules) -> None: sheet = _sheet(r(0, "60.01", A), r(1, "ООО Адыгея", A), r(2, 105012345, A), [L(0), "Итого", *A]) document, _, ok = _parse(sheet, rules) assert ok assert [row.counterparty_inn for row in document.rows if row.group_type == "COUNTERPARTY"] == ["0105012345"] def test_children_of_inn_row_move_up_to_counterparty(rules: Rules) -> None: sheet = _sheet( r(0, "60.01", A), # 9 r(1, "А2 БИЗНЕС ООО", A), # 10 r(2, 2370012036, A), # 11 r(3, "Договор № 1", A), # 12 [L(0), "Итого", *A], detail="Детализация по субсчетам, субконто: Контрагенты, Контрагенты.ИНН, Договоры", ) document, _, ok = _parse(sheet, rules) assert ok assert [g.type for g in document.group_schema] == ["ACCOUNT", "COUNTERPARTY", "CONTRACT"] contract = next(row for row in document.rows if row.row_value == "Договор № 1") assert (contract.group_type, contract.level, contract.parent_row_number) == ("CONTRACT", 2, 10) def test_inn_row_with_other_sums_is_not_glued_and_needs_check(rules: Rules) -> None: sheet = _sheet(r(0, "60.01", A), r(1, "А2 БИЗНЕС ООО", A), r(2, 2370012036, B), [L(0), "Итого", *A]) document, warnings, ok = _parse(sheet, rules) inn_row = next(row for row in document.rows if row.row_value == "2370012036") assert (inn_row.group_type, inn_row.counterparty_name, inn_row.counterparty_inn) == ("OTHER", None, None) parent = next(row for row in document.rows if row.row_value == "А2 БИЗНЕС ООО") assert parent.counterparty_inn is None assert not ok assert any("строка 11" in warning and "Контрагенты.ИНН" in warning for warning in warnings), warnings def test_two_inn_rows_under_one_counterparty_are_not_glued(rules: Rules) -> None: sheet = _sheet( r(0, "60.01", ROOT), r(1, "А2 БИЗНЕС ООО", ROOT), r(2, 2370012036, A), r(2, 7706808138, B), [L(0), "Итого", *ROOT], ) document, _, ok = _parse(sheet, rules) assert not ok assert [row.group_type for row in document.rows if row.level == 2] == ["OTHER", "OTHER"] # noqa: PLR2004 def test_inn_attribute_of_unknown_group_stays_a_group(rules: Rules) -> None: sheet = _sheet( r(0, "60.01", A), r(1, "А2 БИЗНЕС ООО", A), r(2, "Договор № 1", A), [L(0), "Итого", *A], detail="Детализация по субсчетам, субконто: Контрагенты, Договоры.ИНН", ) document, _, _ = _parse(sheet, rules) assert [g.name for g in document.group_schema] == ["Субконто", "Контрагенты", "Договоры.ИНН"] def test_empty_inn_row_is_glued_without_inn(rules: Rules) -> None: # R-1: у физлица 1С выводит строку реквизита с пустой ячейкой ИНН — это «ИНН не указан», а не ошибка разбора sheet = _sheet(r(0, "60.01", A), r(1, "Кривенков Алексей Анатольевич", A), r(2, None, A), [L(0), "Итого", *A]) document, warnings, ok = _parse(sheet, rules) assert ok assert _values(document) == ["60.01", "Кривенков Алексей Анатольевич", "Итого"] assert document.rows[1].counterparty_inn is None assert any("строка 11" in warning and "пуст" in warning for warning in warnings), warnings def test_unrecognised_inn_text_is_not_glued(rules: Rules) -> None: sheet = _sheet(r(0, "60.01", A), r(1, "ООО Нерезидент", A), r(2, "нерез.", A), [L(0), "Итого", *A]) document, warnings, ok = _parse(sheet, rules) assert not ok assert "нерез." in _values(document) assert any("не распознан" in warning for warning in warnings), warnings # --- Шапка варианта 1: группировки — строки блока заголовка («Счет / Контрагенты / Контрагенты.ИНН») --- def _variant1_sheet(*rows: list[object]) -> Sheet: head: list[list[object]] = [ ["ООО Зерновая"], ["Оборотно-сальдовая ведомость по счету 60 за 9 месяцев 2025 г."], [], ["Выводимые данные: БУ (данные бухгалтерского учета)"], [], ["Счет", M("Сальдо на начало периода", 2), M("Обороты за период", 2), M("Сальдо на конец периода", 2)], ["Контрагенты", "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"], ["Контрагенты.ИНН"], ] return sheet_from_rows([*head, *rows]) def test_variant1_inn_attribute_row_is_glued(rules: Rules) -> None: sheet = _variant1_sheet(r(0, "60.01", A), r(1, "А2 БИЗНЕС ООО", A), r(2, 2370012036, A), [L(0), "Итого", *A]) document, _, ok = _parse(sheet, rules) assert document.header.header_variant == 1 assert [(g.type, g.name, g.has_inn) for g in document.group_schema] == [ ("ACCOUNT", "Счет", None), ("COUNTERPARTY", "Контрагенты", True), ] assert ok assert _values(document) == ["60.01", "А2 БИЗНЕС ООО", "Итого"] assert document.rows[1].counterparty_inn == "2370012036" # --- Подсказка в обратном порядке: «Контрагенты.ИНН, Контрагенты» — ИНН над наименованием --- DETAIL_ABOVE = "Детализация по субсчетам, субконто: Контрагенты.ИНН, Контрагенты" def test_inn_attribute_before_owner_gives_hint_above(rules: Rules) -> None: sheet = _sheet( r(0, "60.01", A), r(1, 2370012036, A), r(1, "А2 БИЗНЕС ООО", A), [L(0), "Итого", *A], detail=DETAIL_ABOVE ) document, warnings, ok = _parse(sheet, rules) assert [(g.type, g.name, g.has_inn) for g in document.group_schema] == [ ("ACCOUNT", "Субконто", None), ("COUNTERPARTY", "Контрагенты", True), ] assert ok assert _values(document) == ["60.01", "А2 БИЗНЕС ООО", "Итого"] counterparty = document.rows[1] assert (counterparty.counterparty_inn, counterparty.level, counterparty.parent_row_number) == ("2370012036", 1, 9) assert any("2370012036" in warning for warning in warnings), warnings def test_inn_attribute_before_owner_in_tree_gives_hint_above(rules: Rules) -> None: # ИНН-реквизит стоит над наименованием и является его родителем в дереве (наименование на уровень глубже) sheet = _sheet( r(0, "60.01", A), r(1, 2370012036, A), r(2, "А2 БИЗНЕС ООО", A), [L(0), "Итого", *A], detail=DETAIL_ABOVE ) document, warnings, ok = _parse(sheet, rules) assert ok assert _values(document) == ["60.01", "А2 БИЗНЕС ООО", "Итого"] counterparty = document.rows[1] assert (counterparty.counterparty_inn, counterparty.level, counterparty.parent_row_number) == ("2370012036", 1, 9) assert any("«Контрагенты.ИНН»" in warning for warning in warnings), warnings def test_hint_above_glues_sibling_without_checksum(rules: Rules) -> None: # Подсказка снимает проверку контрольной суммы: «7701234567» — опечатка, но шапка говорит, что это ИНН sheet = _sheet( r(0, "60.01", A), r(1, "7701234567", (None,) * 6), r(1, "ООО Альфа", A), [L(0), "Итого", *A], detail=DETAIL_ABOVE, ) document, _, ok = _parse(sheet, rules) assert ok assert [(row.row_value, row.counterparty_inn) for row in document.rows if row.group_type == "COUNTERPARTY"] == [ ("ООО Альфа", "7701234567") ] def test_variant1_inn_attribute_row_above_owner(rules: Rules) -> None: head: list[list[object]] = [ ["ООО Зерновая"], ["Оборотно-сальдовая ведомость по счету 60 за 9 месяцев 2025 г."], [], ["Выводимые данные: БУ (данные бухгалтерского учета)"], [], ["Счет", M("Сальдо на начало периода", 2), M("Обороты за период", 2), M("Сальдо на конец периода", 2)], ["Контрагенты.ИНН", "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"], ["Контрагенты"], ] sheet = sheet_from_rows( [*head, r(0, "60.01", A), r(1, 2370012036, A), r(1, "А2 БИЗНЕС ООО", A), [L(0), "Итого", *A]] ) document, _, ok = _parse(sheet, rules) assert [(g.type, g.name, g.has_inn) for g in document.group_schema] == [ ("ACCOUNT", "Счет", None), ("COUNTERPARTY", "Контрагенты", True), ] assert ok assert document.rows[1].counterparty_inn == "2370012036"