arm-tabular-parser/api/tests/unit/parser/test_inn_subkonto.py
Andrey Epifancev 26ed11ad28 ОСВ: строки ИНН — подключение прохода, подсказка шапки в обоих порядках, старые склейки удалены (#35)
Хвосты ревью Task 3: формулировка направления листа в spec/design (T3.1), тесты противоречия сведений и приоритета подсказки (T3.2), assert на glue.ok (T3.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:27:02 +04:00

295 lines
13 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.

"""Реквизит субконто «Контрагенты.ИНН» строкой под контрагентом (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"