arm-tabular-parser/api/tests/unit/parser/test_osv_indent_nesting.py
Andrey Epifancev e1760c08f6 fix(osv): вложенность по суммам только без названных групп, без ИНН и при одном отступе; сдвиги складываются (AURORA-1858, #78, ревью)
C-1: nest_by_sums включается из osv.py, если схема шапки не называет группы после счёта; строка с ИНН не вкладывается
и не становится родителем; прямые дети родителя должны быть на одном отступе. M-1: сдвиги складываются.
M-2: код без разделителя сегментов не вкладывается. M-3: строка закрытия — только с ненулевыми оборотами.
M-4: общий жадный перебор _greedy_ends для strategy_sums и вложенности, граница NEST_SCAN_LIMIT под тестом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 10:43:39 +04:00

98 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Вложенность по суммам при иерархии по отступам (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}