"""Вложенность по суммам при иерархии по отступам (AURORA-1858, ревью C-1): защиты на уровне всего листа. Правило не должно прятать реальную ошибку и менять `group_type`/`group_name` строк: при названных группах аналитики в шапке оно отключено, с ИНН строка не вкладывается и не становится родителем. """ import pytest from gridbuilder import C, M from src.contract.result import OsvDocument from src.parser.deadline import Deadline from src.parser.osv import OsvParser from src.parser.rules import Rules, load_rules from gridbuilder import sheet_from_rows # isort: skip TITLE = "Оборотно-сальдовая ведомость по счету 60 за 1 квартал 2025 г." TIMEOUT_SECONDS = 60 Row = list[object] @pytest.fixture(scope="module") def rules() -> Rules: return load_rules() def sums(value: int) -> Row: return [None, None, value, None, value, None] def line(indent: int, text: str, value: int, inn: str | None = None) -> Row: return [C(text, indent=indent), inn, None, *sums(value)] def parse(rules: Rules, head_group_rows: list[Row], body: list[Row]) -> OsvDocument: rows: list[Row] = [ ["ООО Ромашка"], [TITLE], [], [ "Счет", "ИНН", "КПП", M("Сальдо на начало периода", 2), M("Обороты за период", 2), M("Сальдо на конец периода", 2), ], ["Контрагенты", None, None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"], *head_group_rows, *body, ] return OsvParser(rules).parse_sheet(sheet_from_rows(rows), Deadline(TIMEOUT_SECONDS)).document def test_reviewer_scenario_with_named_groups_and_inn_stays_needs_check(rules: Rules) -> None: """Счёт 60 = 100, но контрагенты 60 + 40 + 40: это ошибка файла, а не вложенность «Гамма под Бетой».""" document = parse( rules, [["Договоры"]], [ line(0, "60", 100), line(1, "ООО Альфа", 60, "7701234567"), line(2, "Договор 1", 60), line(1, "ООО Бета", 40, "7707654321"), line(1, "ООО Гамма", 40, "5001234567"), [C("Итого"), None, None, *sums(100)], ], ) by_text = {row.row_value: row for row in document.rows} gamma = by_text["ООО Гамма"] assert document.status == "needs_check" assert gamma.group_type == "COUNTERPARTY" assert gamma.counterparty_inn == "5001234567" assert gamma.parent_row_number == by_text["60"].row_number assert by_text["ООО Бета"].parent_row_number == by_text["60"].row_number def test_named_analytic_group_in_header_disables_nesting_by_sums(rules: Rules) -> None: """Без ИНН и с одним отступом правило бы вложило «Б» в «В» (40 ⊃ 40): шапка называет «Контрагенты», вложение сменило бы group_type строки, поэтому дерево остаётся по отступам и лист на проверке.""" document = parse( rules, [], [ line(0, "60", 100), line(1, "Контрагент А", 60), line(1, "Контрагент Б", 40), line(1, "Контрагент В", 40), [C("Итого"), None, None, *sums(100)], ], ) by_text = {row.row_value: row for row in document.rows} assert document.status == "needs_check" assert {by_text[name].group_type for name in ("Контрагент А", "Контрагент Б", "Контрагент В")} == {"COUNTERPARTY"} assert {by_text[name].level for name in ("Контрагент А", "Контрагент Б", "Контрагент В")} == {1}