В шапке варианта 2 «субконто: Контрагенты, Контрагенты.ИНН» 1С выводит ИНН отдельной строкой на уровень глубже контрагента. Парсер принимал реквизит за второй уровень контрагентов: строка ИНН становилась контрагентом с именем «2370012036.0» и без ИНН, у наименования ИНН тоже не было, в свод ядра попадали обе строки. - header: элемент «<группировка>.ИНН» — InnAttribute, в group_schema не попадает, у владельца has_inn = true; GroupSchema.positions держит позицию для классификатора; - classify: строка реквизита — OTHER с ИНН ячейки (column_inn, R-24); glue_inn_attributes отдаёт ИНН контрагенту, убирает строку, поднимает её детей; не склеилось (несколько строк, суммы не равны, ИНН не распознан или расходится) — OTHER, предупреждение, needs_check; пустая ячейка (физлицо) — склейка без ИНН (R-1); - readers: целый float из xls читается как int — без «.0». Файл банка: 713 из 716 контрагентов с ИНН, 3 физлица без ИНН, status done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 lines
7.9 KiB
Python
182 lines
7.9 KiB
Python
"""Реквизит субконто «Контрагенты.ИНН» строкой под контрагентом (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
|