ОСВ: реквизит «Контрагенты.ИНН» и в шапке варианта 1 (#32)

Строка блока заголовка «Контрагенты.ИНН» под «Контрагенты» — тот же реквизит,
что элемент списка субконто варианта 2: в group_schema не попадает, у владельца
has_inn, строки склеиваются glue_inn_attributes. Общая часть вариантов —
_attach_inn_attribute и _positions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-13 12:03:10 +04:00
parent bb78f81094
commit 02ad8be018
3 changed files with 69 additions and 21 deletions

View File

@ -172,8 +172,8 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
`COUNTERPARTY`; договоры и прочее не трогаются. Правила — `osv_headers.yaml`: `counterparty_inn_suffix`, `inn_only_row`,
`inn_kpp_cell`.
**Реквизит «Контрагенты.ИНН»** (OQ-162, #32): в шапке варианта 2 элемент `<группировка>.ИНН` («субконто: Контрагенты,
Контрагенты.ИНН») — не группировка, а реквизит: в `group_schema` его нет, у владельца `has_inn = true`
**Реквизит «Контрагенты.ИНН»** (OQ-162, #32): элемент `<группировка>.ИНН` в шапке — в варианте 2 («субконто: Контрагенты,
Контрагенты.ИНН») или строкой блока заголовка в варианте 1 — не группировка, а реквизит: в `group_schema` его нет, у владельца `has_inn = true`
(`header.GroupSchema.positions` держит его позицию для классификатора). 1С выводит ИНН строкой под контрагентом, с теми же
суммами; `classify.glue_inn_attributes` отдаёт ИНН контрагенту (извлечение — как из колонки ИНН, R-24), строку убирает,
её детей поднимает на уровень; предупреждение «ИНН … отнесён к контрагенту в строке …». Пустая ячейка (физлицо без ИНН) —

View File

@ -481,16 +481,6 @@ def _first_group_name(sheet: Sheet, header: TableHeader) -> str | None:
return sheet.text_at(header.group_label_rows[0], header.text_col) or None
def _variant1_groups(sheet: Sheet, header: TableHeader, rules: Rules) -> list[OsvGroup]:
groups: list[OsvGroup] = []
for index, row in enumerate(header.group_label_rows):
label = sheet.text_at(row, header.text_col)
# Первая группировка — всегда ACCOUNT, как бы она ни называлась («Счет», «Подразделение», «Субконто»)
group_type: GroupType = "ACCOUNT" if index == 0 else rules.groups.classify(label)
groups.append(_group(group_type, label, has_inn=_row_has_inn(sheet, row, rules)))
return groups or [_group("ACCOUNT")]
@dataclass(frozen=True)
class InnAttribute:
"""Реквизит ИНН группировки контрагентов («Контрагенты.ИНН», OQ-162): 1С выводит его строкой под строкой
@ -524,32 +514,58 @@ def _inn_owner(item: str, groups: list[OsvGroup], rules: Rules) -> int | None:
return None
def _attach_inn_attribute(item: str, groups: list[OsvGroup], slots: list[int | InnAttribute], rules: Rules) -> bool:
"""`item` — реквизит ИНН одной из предыдущих группировок контрагентов: владелец получает `has_inn`, позиция строк —
`InnAttribute`; `True`. Иначе ничего не меняется, `False`."""
owner = _inn_owner(item, groups, rules)
if owner is None:
return False
groups[owner] = _group("COUNTERPARTY", groups[owner].name, has_inn=True)
slots.append(InnAttribute(item))
return True
def _positions(groups: list[OsvGroup], slots: list[int | InnAttribute]) -> list[OsvGroup | InnAttribute]:
"""Позиции строк: индекс группировки в `groups` или реквизит ИНН (OQ-162)."""
return [slot if isinstance(slot, InnAttribute) else groups[slot] for slot in slots]
def _variant1_groups(sheet: Sheet, header: TableHeader, rules: Rules) -> GroupSchema:
groups: list[OsvGroup] = []
slots: list[int | InnAttribute] = []
for index, row in enumerate(header.group_label_rows):
label = sheet.text_at(row, header.text_col)
if index > 0 and _attach_inn_attribute(label, groups, slots, rules):
continue
# Первая группировка — всегда ACCOUNT, как бы она ни называлась («Счет», «Подразделение», «Субконто»)
group_type: GroupType = "ACCOUNT" if index == 0 else rules.groups.classify(label)
groups.append(_group(group_type, label, has_inn=_row_has_inn(sheet, row, rules)))
slots.append(len(groups) - 1)
if not groups:
groups, slots = [_group("ACCOUNT")], [0]
return GroupSchema(groups, _positions(groups, slots), 1)
def _variant2_groups(sheet: Sheet, header: TableHeader, rules: Rules, items: list[str]) -> GroupSchema:
groups = [_group("ACCOUNT", _first_group_name(sheet, header))]
# Позиция строки: индекс группировки в `groups` или реквизит ИНН (OQ-162)
slots: list[int | InnAttribute] = [0]
inn_pending = header.inn_col is not None
for item in items:
owner = _inn_owner(item, groups, rules)
if owner is not None:
groups[owner] = _group("COUNTERPARTY", groups[owner].name, has_inn=True)
slots.append(InnAttribute(item))
if _attach_inn_attribute(item, groups, slots, rules):
continue
group_type = rules.groups.classify(item)
has_inn = inn_pending and group_type == "COUNTERPARTY"
inn_pending = inn_pending and not has_inn
groups.append(_group(group_type, item, has_inn=has_inn))
slots.append(len(groups) - 1)
positions = [slot if isinstance(slot, InnAttribute) else groups[slot] for slot in slots]
return GroupSchema(groups, positions, 2)
return GroupSchema(groups, _positions(groups, slots), 2)
def build_group_levels(sheet: Sheet, header: TableHeader, rules: Rules) -> GroupSchema:
"""Группировки (первая — ACCOUNT), позиции строк и вариант шапки (1 — строки блока, 2 — «субконто: …»)."""
items = _subkonto_items(_head_cells(sheet, header), rules)
if items is None:
groups = _variant1_groups(sheet, header, rules)
return GroupSchema(groups, list(groups), 1)
return _variant1_groups(sheet, header, rules)
return _variant2_groups(sheet, header, rules, items)

View File

@ -179,3 +179,35 @@ def test_unrecognised_inn_text_is_not_glued(rules: Rules) -> None:
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"