ОСВ: правки финального ревью строк ИНН — реквизит по схеме, подсказка «над», страховка владельца, родитель после склейки (#35)

R-6 (design #22), R-7 (#23), R-8 (#24), R-9 (#25), R-10 (#26), H.1/H.2 (#27); словарь позиций соседей (T2.6/T3.3);
spec/design/README: текст предупреждения, цена ошибки design #1, ссылки на решение владельца 10 (M-3, M-4, M-5, M-8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-13 16:20:39 +04:00
parent 79ffe602f6
commit ad1a6ec10e
7 changed files with 229 additions and 33 deletions

View File

@ -166,8 +166,9 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
без подписи в конце таблицы (1С без «Итого») принимается за итоговую с предупреждением. без подписи в конце таблицы (1С без «Итого») принимается за итоговую с предупреждением.
**ИНН контрагента** (OQ-100, OQ-130): `counterparty_inn` берётся из колонки ИНН (в том числе склеенного «ИНН/КПП»), затем из **ИНН контрагента** (OQ-100, OQ-130): `counterparty_inn` берётся из колонки ИНН (в том числе склеенного «ИНН/КПП»), затем из
строки без сумм прямо над названием на том же уровне оформления (такая строка в `rows` не попадает, в строки ИНН рядом с наименованием (любая из восьми раскладок, `inn_rows.py`, OQ-163: такая строка в `rows` не попадает, в
`metrics.warnings` — «ИНН … над строкой … отнесён к ней»), затем из конца текста названия («Полиграф, 3528315710», `metrics.warnings` — «строка N: ИНН … отнесён к контрагенту в строке M»; правило OQ-130 «строка без сумм над названием»
заменено), затем из конца текста названия («Полиграф, 3528315710»,
«ООО Альфа (ИНН 7701234567)»; 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»). Из текста ИНН «ООО Альфа (ИНН 7701234567)»; 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»). Из текста ИНН
отрезается всегда: `counterparty_name` без ИНН и разделителя, `row_value` — исходный текст. Только для строк отрезается всегда: `counterparty_name` без ИНН и разделителя, `row_value` — исходный текст. Только для строк
`COUNTERPARTY`; договоры и прочее не трогаются. Правила — `osv_headers.yaml`: `counterparty_inn_suffix`, `inn_only_row`, `COUNTERPARTY`; договоры и прочее не трогаются. Правила — `osv_headers.yaml`: `counterparty_inn_suffix`, `inn_only_row`,
@ -175,11 +176,13 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
**Реквизит «Контрагенты.ИНН»** (OQ-162, #32): элемент `<группировка>.ИНН` в шапке — в варианте 2 («субконто: Контрагенты, **Реквизит «Контрагенты.ИНН»** (OQ-162, #32): элемент `<группировка>.ИНН` в шапке — в варианте 2 («субконто: Контрагенты,
Контрагенты.ИНН») или строкой блока заголовка в варианте 1 — не группировка, а реквизит: в `group_schema` его нет, у владельца `has_inn = true` Контрагенты.ИНН») или строкой блока заголовка в варианте 1 — не группировка, а реквизит: в `group_schema` его нет, у владельца `has_inn = true`
(`header.GroupSchema.positions` держит его позицию для классификатора). подсказка для строк ИНН: порядок элементов задаёт направление (после владельца — ИНН под наименованием, перед — над) и снимает (владелец и направление — `GroupSchema.inn_hint`). Подсказка для строк ИНН: порядок элементов задаёт направление (после владельца — ИНН под наименованием, перед — над) и снимает
проверку контрольной суммы. Склейку выполняет `inn_rows.py` (OQ-163): ИНН строки отдаётся наименованию (извлечение — как из проверку контрольной суммы. Склейку выполняет `inn_rows.py` (OQ-163): ИНН строки отдаётся наименованию (извлечение — как из
колонки ИНН, R-24), строка убирается, её дети переходят на уровень; предупреждение «ИНН … отнесён к контрагенту в строке …». колонки ИНН, R-24), строка убирается, её дети переходят на уровень; предупреждение «ИНН … отнесён к контрагенту в строке …».
Пустая ячейка (физлицо без ИНН) — склейка без ИНН. Не склеилось (несколько строк ИНН, суммы не равны, ИНН не распознан или Пустая ячейка (физлицо без ИНН) — склейка без ИНН. Не склеилось (несколько строк ИНН, суммы не равны, ИНН не распознан, направление неизвестно) — строка `OTHER`,
расходится, направление неизвестно) — строка `OTHER`, предупреждение с причиной, документ `needs_check`; раскладка, противоречащая предупреждение с причиной, документ `needs_check`; ИНН строки, расходящийся с ИНН колонки у наименования, не применяется —
остаётся ИНН колонки, предупреждение, `needs_check`; строка из 10/12 цифр с неверной контрольной суммой (без подсказки) остаётся
обычной строкой, «строка N: похожа на ИНН X, но контрольная сумма не сходится — строка оставлена как есть», `needs_check`; раскладка, противоречащая
подсказке шапки, склеивается с отдельным предупреждением и `needs_check`. Число из xls читается без «.0» у целых (`readers/_common.make_cell`). подсказке шапки, склеивается с отдельным предупреждением и `needs_check`. Число из xls читается без «.0» у целых (`readers/_common.make_cell`).
**Период-диапазон.** «Октябрь 2024 г. – Сентябрь 2025 г.» (`periods.yaml`: `months`, `range`): январь–конец квартала — **Период-диапазон.** «Октябрь 2024 г. – Сентябрь 2025 г.» (`periods.yaml`: `months`, `range`): январь–конец квартала —

View File

@ -21,7 +21,7 @@ from src.contract.result import OsvGroup
from src.parser.classify import row_data_type from src.parser.classify import row_data_type
from src.parser.grid import Cell, Sheet from src.parser.grid import Cell, Sheet
from src.parser.header import InnHint from src.parser.header import InnHint
from src.parser.hierarchy import BodyRow, Levels, parents from src.parser.hierarchy import BodyRow, Levels
from src.parser.numbers import column_inn, inn_checksum_ok, row_inn from src.parser.numbers import column_inn, inn_checksum_ok, row_inn
from src.parser.rules import Rules from src.parser.rules import Rules
@ -44,6 +44,7 @@ class InnGlue:
unpaired: frozenset[int] unpaired: frozenset[int]
warnings: list[str] warnings: list[str]
ok: bool ok: bool
sources: dict[int, int] = field(default_factory=dict) # строка наименования -> строка ИНН, давшая ему ИНН
@dataclass @dataclass
@ -57,6 +58,10 @@ class _Tree:
kind: dict[int, Kind] kind: dict[int, Kind]
inn: dict[int, str | None] = field(default_factory=dict) # ИНН строки ИНН; None — пустой или не распознан inn: dict[int, str | None] = field(default_factory=dict) # ИНН строки ИНН; None — пустой или не распознан
lookalike: dict[int, str] = field(default_factory=dict) # 10/12 цифр с неверной контрольной суммой lookalike: dict[int, str] = field(default_factory=dict) # 10/12 цифр с неверной контрольной суммой
position: dict[int, int] = field(default_factory=dict) # строка -> индекс среди детей её родителя
def siblings(self, row: int) -> list[int]:
return self.children.get(self.parent[row], [])
def kids(self, row: int) -> list[int]: def kids(self, row: int) -> list[int]:
"""Дети строки, кроме Вал./Кол. (они не уровень аналитики).""" """Дети строки, кроме Вал./Кол. (они не уровень аналитики)."""
@ -104,6 +109,8 @@ def _build( # noqa: PLR0913, PLR0917 - всё нужное для вида ст
for item in rows: for item in rows:
children.setdefault(levels.parent[item.row], []).append(item.row) children.setdefault(levels.parent[item.row], []).append(item.row)
tree = _Tree(rows, {item.row: item for item in rows}, dict(levels.parent), children, {}) tree = _Tree(rows, {item.row: item for item in rows}, dict(levels.parent), children, {})
for siblings in children.values():
tree.position.update((row, index) for index, row in enumerate(siblings))
for item in rows: for item in rows:
tree.kind[item.row] = _kind(item, total_row, sheet, hint, rules, currency, tree) tree.kind[item.row] = _kind(item, total_row, sheet, hint, rules, currency, tree)
if hint is not None and hint.direction == "below": if hint is not None and hint.direction == "below":
@ -167,9 +174,10 @@ def _no_extra_level(depth: dict[int, int], schema: Sequence[OsvGroup]) -> bool:
def _owned_by_parent(parent: int, depth: dict[int, int], schema: Sequence[OsvGroup], hint: InnHint | None) -> bool: def _owned_by_parent(parent: int, depth: dict[int, int], schema: Sequence[OsvGroup], hint: InnHint | None) -> bool:
"""Строка ИНН с детьми под наименованием: ИНН родителя («под», дети — договоры) или ребёнка («над»)? Подсказка """Строка ИНН с детьми под наименованием: ИНН родителя («под», дети — договоры) или ребёнка («над»)? Подсказка
шапки решает; без неё — «под», если родитель по схеме — контрагент (design, решение без обсуждения №10).""" «под» — родителя; иначе «под», если родитель по схеме — контрагент (design №10). Подсказка «над» против родителя-
if hint is not None: контрагента не уводит ИНН ребёнку-договору: пара — родитель, а противоречие подсвечивает H-2 (R-7, design №23)."""
return hint.direction == "below" if hint is not None and hint.direction == "below":
return True
position = depth[parent] position = depth[parent]
return position < len(schema) and schema[position].type == "COUNTERPARTY" return position < len(schema) and schema[position].type == "COUNTERPARTY"
@ -184,20 +192,26 @@ def _parent_can_own(parent: int, depth: dict[int, int], schema: Sequence[OsvGrou
return position >= len(schema) or schema[position].type == "COUNTERPARTY" return position >= len(schema) or schema[position].type == "COUNTERPARTY"
def _unrecognised_attributes(tree: _Tree, hint: InnHint | None, pairs: _Pairs) -> None: def _unrecognised_attributes(tree: _Tree, schema: Sequence[OsvGroup], hint: InnHint | None, pairs: _Pairs) -> None:
"""С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью не-ИНН — строка """С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью не-ИНН — строка
реквизита с нераспознанным ИНН («нерез.»): без пары, как в #32 (design, решение без обсуждения №9).""" реквизита с нераспознанным ИНН («нерез.»): без пары, как в #32 (design №9). Кандидат — ребёнок только у
наименования, которое по схеме контрагент (R-6, design №22); тогда ребёнок может иметь детей (договоры остаются
договорами). Позиция вне схемы или схема без позиции — прежнее условие R-1 (design №14): ребёнок — лист."""
if hint is None or hint.direction != "below": if hint is None or hint.direction != "below":
return return
depth = _depths(tree)
for item in tree.rows: for item in tree.rows:
if tree.kind[item.row] != "name": if tree.kind[item.row] != "name":
continue continue
position = depth[item.row]
if position < len(schema) and schema[position].type != "COUNTERPARTY":
continue
kids = tree.kids(item.row) kids = tree.kids(item.row)
if len(kids) != 1: if len(kids) != 1:
continue continue
kid = tree.by_row[kids[0]] kid = tree.by_row[kids[0]]
# ребёнок со своими детьми — подлинное наименование (у него своя строка ИНН), не реквизит (R-1, design #14) allowed = position < len(schema) or not tree.kids(kid.row)
if tree.kind[kid.row] == "name" and not tree.kids(kid.row) and kid.text and _same_sums(kid, item): if tree.kind[kid.row] == "name" and allowed and kid.text and _same_sums(kid, item):
tree.kind[kid.row] = "candidate" tree.kind[kid.row] = "candidate"
tree.inn[kid.row] = None tree.inn[kid.row] = None
pairs.reason[kid.row] = "ИНН не распознан (нужно 10 или 12 цифр)" pairs.reason[kid.row] = "ИНН не распознан (нужно 10 или 12 цифр)"
@ -238,8 +252,8 @@ def _pair_in_tree(tree: _Tree, schema: Sequence[OsvGroup], hint: InnHint | None,
def _neighbours(tree: _Tree, row: int) -> tuple[int | None, int | None]: def _neighbours(tree: _Tree, row: int) -> tuple[int | None, int | None]:
"""Соседние строки-наименования того же родителя: выше и ниже (`None` — соседа нет или он не наименование).""" """Соседние строки-наименования того же родителя: выше и ниже (`None` — соседа нет или он не наименование)."""
siblings = tree.children.get(tree.parent[row], []) siblings = tree.siblings(row)
index = siblings.index(row) index = tree.position[row]
before = siblings[index - 1] if index > 0 else None before = siblings[index - 1] if index > 0 else None
after = siblings[index + 1] if index + 1 < len(siblings) else None after = siblings[index + 1] if index + 1 < len(siblings) else None
return ( return (
@ -342,8 +356,8 @@ def _drop_conflicts(pairs: _Pairs) -> None:
def _near_name(tree: _Tree, row: int) -> bool: def _near_name(tree: _Tree, row: int) -> bool:
"""Рядом с наименованием: родитель, ребёнок или соседняя строка того же родителя — наименование.""" """Рядом с наименованием: родитель, ребёнок или соседняя строка того же родителя — наименование."""
parent = tree.parent[row] parent = tree.parent[row]
siblings = tree.children.get(parent, []) siblings = tree.siblings(row)
index = siblings.index(row) index = tree.position[row]
near = [*siblings[max(index - 1, 0) : index], *siblings[index + 1 : index + 2], *tree.children.get(row, [])] near = [*siblings[max(index - 1, 0) : index], *siblings[index + 1 : index + 2], *tree.children.get(row, [])]
if parent is not None: if parent is not None:
near.append(parent) near.append(parent)
@ -351,11 +365,14 @@ def _near_name(tree: _Tree, row: int) -> bool:
def _without(rows: Sequence[BodyRow], levels: Levels, removed: set[int]) -> tuple[list[BodyRow], Levels]: def _without(rows: Sequence[BodyRow], levels: Levels, removed: set[int]) -> tuple[list[BodyRow], Levels]:
"""Строки и дерево без `removed`: потомки убранной строки поднимаются на уровень, родители пересчитываются.""" """Строки и дерево без `removed`: потомки убранной строки поднимаются на уровень; новый родитель — ближайший
неудалённый предок по прежнему дереву, а не пересчёт по уровням (иначе пропуск уровней outline даёт чужого
родителя; R-9, design №25)."""
if not removed: if not removed:
return list(rows), levels return list(rows), levels
lifted: dict[int, int] = {} lifted: dict[int, int] = {}
by_row: dict[int, int] = {} by_row: dict[int, int] = {}
parent_of: dict[int, int | None] = {}
kept: list[BodyRow] = [] kept: list[BodyRow] = []
for item in rows: for item in rows:
parent = levels.parent[item.row] parent = levels.parent[item.row]
@ -364,8 +381,11 @@ def _without(rows: Sequence[BodyRow], levels: Levels, removed: set[int]) -> tupl
if item.row in removed: if item.row in removed:
continue continue
by_row[item.row] = levels.by_row[item.row] - lift by_row[item.row] = levels.by_row[item.row] - lift
while parent is not None and parent in removed:
parent = levels.parent[parent]
parent_of[item.row] = parent
kept.append(item) kept.append(item)
return kept, Levels(by_row=by_row, parent=parents(kept, by_row), strategy=levels.strategy) return kept, Levels(by_row=by_row, parent=parent_of, strategy=levels.strategy)
def _contradicts(row: int, partner: int, hint: InnHint) -> bool: def _contradicts(row: int, partner: int, hint: InnHint) -> bool:
@ -376,7 +396,7 @@ def _contradicts(row: int, partner: int, hint: InnHint) -> bool:
def _contradiction(row: int, partner: int, inn: str, hint: InnHint) -> str: def _contradiction(row: int, partner: int, inn: str, hint: InnHint) -> str:
"""Склейка состоялась, но раскладка не та, что указана в шапке (design, решение 10): подсветить, проверка.""" """Склейка состоялась, но раскладка не та, что указана в шапке (решение владельца 10, H-2): подсветить, проверка."""
where, expected = ("под", "над") if hint.direction == "above" else ("над", "под") where, expected = ("под", "над") if hint.direction == "above" else ("над", "под")
return ( return (
f"строка {row}: ИНН {inn} стоит {where} наименованием в строке {partner}, а шапка («{hint.name}») " f"строка {row}: ИНН {inn} стоит {where} наименованием в строке {partner}, а шапка («{hint.name}») "
@ -397,6 +417,7 @@ def _result( # noqa: PLR0913, PLR0917 - сборка результата из
label = "" if hint is None else f" («{hint.name}»)" label = "" if hint is None else f" («{hint.name}»)"
warnings: list[str] = [] warnings: list[str] = []
inns: dict[int, str] = {} inns: dict[int, str] = {}
sources: dict[int, int] = {}
ok = not pairs.reason ok = not pairs.reason
for item in rows: for item in rows:
row = item.row row = item.row
@ -404,6 +425,8 @@ def _result( # noqa: PLR0913, PLR0917 - сборка результата из
if row in pairs.partner: if row in pairs.partner:
partner = pairs.partner[row] partner = pairs.partner[row]
if inn is None: if inn is None:
# пустой кандидат бывает только «под, в дереве» и склеивается с родителем: конфликта с колонкой нет
# (H.1 финального ревью: случай «расхождение с колонкой» для пустого ИНН недостижим)
warnings.append( warnings.append(
f"строка {row}: ИНН пуст{label} — строка отнесена к контрагенту в строке {partner} без ИНН" f"строка {row}: ИНН пуст{label} — строка отнесена к контрагенту в строке {partner} без ИНН"
) )
@ -417,6 +440,7 @@ def _result( # noqa: PLR0913, PLR0917 - сборка результата из
) )
continue continue
inns[partner] = inn inns[partner] = inn
sources[partner] = row
warnings.append(f"строка {row}: ИНН {inn}{label} отнесён к контрагенту в строке {partner}") warnings.append(f"строка {row}: ИНН {inn}{label} отнесён к контрагенту в строке {partner}")
if hint is not None and _contradicts(row, partner, hint): if hint is not None and _contradicts(row, partner, hint):
ok = False ok = False
@ -431,7 +455,7 @@ def _result( # noqa: PLR0913, PLR0917 - сборка результата из
"оставлена как есть" "оставлена как есть"
) )
kept, kept_levels = _without(rows, levels, set(pairs.partner)) kept, kept_levels = _without(rows, levels, set(pairs.partner))
return InnGlue(kept, kept_levels, inns, frozenset(pairs.reason), warnings, ok) return InnGlue(kept, kept_levels, inns, frozenset(pairs.reason), warnings, ok, sources)
def glue_inn_rows( # noqa: PLR0913 - интерфейс плана: всё, что нужно проходу, приходит явно def glue_inn_rows( # noqa: PLR0913 - интерфейс плана: всё, что нужно проходу, приходит явно
@ -452,7 +476,7 @@ def glue_inn_rows( # noqa: PLR0913 - интерфейс плана: всё, ч
""" """
tree = _build(rows, levels, total_row, sheet, hint, rules) tree = _build(rows, levels, total_row, sheet, hint, rules)
pairs = _Pairs() pairs = _Pairs()
_unrecognised_attributes(tree, hint, pairs) _unrecognised_attributes(tree, schema, hint, pairs)
_pair_in_tree(tree, schema, hint, pairs) _pair_in_tree(tree, schema, hint, pairs)
_pair_flat(tree, hint, pairs) _pair_flat(tree, hint, pairs)
_check_sums(tree, pairs) _check_sums(tree, pairs)

View File

@ -19,7 +19,7 @@ from src.parser.errors import InvalidFormatError
from src.parser.grid import Sheet, column_letter from src.parser.grid import Sheet, column_letter
from src.parser.header import ACCOUNT_NOT_FOUND_WARNING, DocHeader, build_group_levels, parse_doc_header from src.parser.header import ACCOUNT_NOT_FOUND_WARNING, DocHeader, build_group_levels, parse_doc_header
from src.parser.hierarchy import resolve_levels from src.parser.hierarchy import resolve_levels
from src.parser.inn_rows import glue_inn_rows from src.parser.inn_rows import InnGlue, glue_inn_rows
from src.parser.rules import Rules from src.parser.rules import Rules
__all__ = ["OsvParser", "data_rows", "sheet_line", "unique"] __all__ = ["OsvParser", "data_rows", "sheet_line", "unique"]
@ -126,6 +126,20 @@ def _invalid_format(sheet: Sheet, step: str, exc: InvalidFormatError) -> Invalid
return InvalidFormatError(f"лист «{sheet.name}», шаг «{step}»: {reason}") return InvalidFormatError(f"лист «{sheet.name}», шаг «{step}»: {reason}")
def _check_inn_owners(glue: InnGlue, classified: list[ClassifiedRow]) -> InnGlue:
"""Страховка (R-8, design №24): ИНН склеен со строкой, которая по схеме не контрагент (подразделение, договор,
OTHER) — ИНН пропадёт молча, поэтому предупреждение и `needs_check`."""
types = {item.body.row: item.group_type for item in classified}
extra = [
f"строка {glue.sources[owner]}: ИНН {inn} отнесён к строке {owner}, но она не контрагент — проверьте"
for owner, inn in glue.inns.items()
if owner in glue.sources and types.get(owner) != "COUNTERPARTY"
]
if not extra:
return glue
return replace(glue, warnings=[*glue.warnings, *extra], ok=False)
class OsvParser: class OsvParser:
"""Конвейер листа ОСВ по словарям `rules`; книгу разбирает `BookParser`.""" """Конвейер листа ОСВ по словарям `rules`; книгу разбирает `BookParser`."""
@ -174,6 +188,7 @@ class OsvParser:
) )
except InvalidFormatError as exc: except InvalidFormatError as exc:
raise _invalid_format(sheet, step, exc) from exc raise _invalid_format(sheet, step, exc) from exc
glue = _check_inn_owners(glue, classified)
doc = _with_table_account(doc, classified) doc = _with_table_account(doc, classified)
deadline.check(f"лист «{sheet.name}»: контрольные суммы") deadline.check(f"лист «{sheet.name}»: контрольные суммы")
checks, check_warnings = run_checks(classified, rules.checks) checks, check_warnings = run_checks(classified, rules.checks)

View File

@ -528,3 +528,97 @@ def test_hint_agreeing_with_layout_has_no_conflict_warning(rules: Rules) -> None
for glue in (below, above, blank): for glue in (below, above, blank):
assert glue.ok assert glue.ok
assert not has_warning(glue, "проверьте") assert not has_warning(glue, "проверьте")
# --- Финальное ревью: R-6 … R-10, T2.6 ---
SCHEMA_DEPARTMENT = [
OsvGroup(type="ACCOUNT", name="Счет"),
OsvGroup(type="DEPARTMENT", name="Подразделения"),
OsvGroup(type="COUNTERPARTY", name="Контрагенты"),
]
def test_hint_below_unrecognised_attribute_with_contracts_under_it_is_other(rules: Rules) -> None:
# R-6 (design #22): родитель — контрагент: «нерез.» с договором под ней — реквизит (OTHER), договор — договор
glue = run(
rules,
r(0, "60", A),
r(1, "ООО Нерезидент", A),
r(2, "нерез.", A),
r(3, "Договор 1", A),
hint=BELOW,
schema=SCHEMA_WITH_CONTRACTS,
)
assert glue.unpaired == frozenset({3})
assert glue.inns == {}
assert not glue.ok
assert (glue.levels.by_row[4], glue.levels.parent[4]) == (3, 3) # дерево не тронуто: классификатор пропустит OTHER
def test_hint_below_single_child_of_department_is_not_an_attribute(rules: Rules) -> None:
# R-6 (design #22): родитель по схеме — подразделение, его единственный ребёнок-лист — контрагент, не реквизит
glue = run(rules, r(0, "60", A), r(1, "БП Аренда", A), r(2, "ООО Альфа", A), hint=BELOW, schema=SCHEMA_DEPARTMENT)
assert (glue.inns, glue.unpaired, glue.ok) == ({}, frozenset(), True)
assert texts(glue) == ["60", "БП Аренда", "ООО Альфа"]
def test_hint_below_inn_with_single_contract_of_same_sums_is_glued_without_check(rules: Rules) -> None:
# R-6 (design #22): контрагент -> ИНН -> один договор с теми же суммами: ИНН у контрагента, договор остаётся
glue = run(
rules,
r(0, "60", A),
r(1, "ООО Альфа", A),
r(2, INN_A, A),
r(3, "Договор 1", A),
hint=BELOW,
schema=SCHEMA_WITH_CONTRACTS,
)
assert (glue.inns, glue.unpaired, glue.ok) == ({2: INN_A}, frozenset(), True)
assert texts(glue) == ["60", "ООО Альфа", "Договор 1"]
def test_hint_above_inn_with_contract_under_counterparty_stays_with_parent_and_is_flagged(rules: Rules) -> None:
# R-7 (design #23): подсказка «над», контрагент -> ИНН -> договор: пара — родитель по схеме, H-2 подсвечивает
glue = run(
rules,
r(0, "60", A),
r(1, "ООО Альфа", A),
r(2, INN_A, A),
r(3, "Договор 1", A),
hint=ABOVE,
schema=SCHEMA_WITH_CONTRACTS,
)
assert (glue.inns, glue.unpaired, glue.ok) == ({2: INN_A}, frozenset(), False)
assert texts(glue) == ["60", "ООО Альфа", "Договор 1"]
assert conflict_warning(glue, 3, 2, "под", "над")
def test_levels_skipping_keep_nearest_surviving_ancestor(rules: Rules) -> None:
# R-9 (design #25): outline с пропуском уровней — новый родитель ближайший неудалённый предок (строка ИНН убрана)
glue = run(
rules,
r(0, "60", A),
r(1, "Физлицо", A),
r(3, "Договор", A),
r(1, INN_A, A),
r(3, "ООО Альфа", A),
)
assert glue.inns == {5: INN_A}
kept = {item.text: glue.levels.parent[item.row] for item in glue.rows}
assert kept == {"60": None, "Физлицо": 1, "Договор": 2, "ООО Альфа": 1}
def test_column_mismatch_plus_contradiction_gives_one_warning(rules: Rules) -> None:
# H.2: расхождение с колонкой ИНН и противоречие подсказке — предупреждение только о расхождении
glue = run(rules, r(0, "60", A), r(1, "ООО Альфа", A, INN_B), r(2, INN_A, A), hint=ABOVE)
assert glue.inns == {}
assert not glue.ok
assert has_warning(glue, "расходится с ИНН", INN_B)
assert not has_warning(glue, "шапка («")

View File

@ -0,0 +1,47 @@
"""Строки ИНН на уровне листа ОСВ: страховка после классификации (R-8, design №24; change osv-inn-rows-layouts, #35)."""
import pytest
from gridbuilder import L, M, sheet_from_rows
from src.parser.deadline import Deadline
from src.parser.osv import OsvParser
from src.parser.rules import Rules, load_rules
TIMEOUT_SECONDS = 60
DETAIL = "Детализация по субсчетам, субконто: Подразделения, Контрагенты"
A = (None, None, 760000, 760000, None, None)
INN_A = 7701234560
@pytest.fixture(scope="module")
def rules() -> Rules:
return load_rules()
def test_inn_glued_to_row_that_is_not_a_counterparty_needs_check(rules: Rules) -> None:
head: list[list[object]] = [
["ООО Зерновая"],
["Оборотно-сальдовая ведомость по счету 60"],
["Период: 9 месяцев 2025 г."],
[DETAIL],
["Выводимые данные: сумма"],
[],
["Субконто", M("Сальдо на начало периода", 2), M("Оборот за период", 2), M("Сальдо на конец периода", 2)],
[None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
]
sheet = sheet_from_rows(
[
*head,
[L(0), "60.01", *A], # 9
[L(1), INN_A, *A], # 10
[L(1), "ООО Альфа", *A], # 11
[L(0), "Итого", *A],
]
)
document, warnings, ok = OsvParser(rules).parse_sheet(sheet, Deadline(TIMEOUT_SECONDS))
assert [(row.row_value, row.group_type) for row in document.rows][1:2] == [("ООО Альфа", "DEPARTMENT")]
assert not ok
expected = f"строка 10: ИНН {INN_A} отнесён к строке 11, но она не контрагент — проверьте"
assert expected in warnings, warnings

View File

@ -92,7 +92,7 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
(прежний порядок §1.3). (прежний порядок §1.3).
9. **Склейка**: строка ИНН уходит из строк и `Levels`, ИНН — наименованию, предупреждение «строка N: ИНН … отнесён к 9. **Склейка**: строка ИНН уходит из строк и `Levels`, ИНН — наименованию, предупреждение «строка N: ИНН … отнесён к
контрагенту в строке M» (пустой при подсказке — «ИНН пуст … без ИНН»). Если раскладка противоречит подсказке шапки контрагенту в строке M» (пустой при подсказке — «ИНН пуст … без ИНН»). Если раскладка противоречит подсказке шапки
(пара выше/ниже, чем указывает подсказка) — следом отдельное предупреждение о противоречии, `ok = False` (решение 10). (пара выше/ниже, чем указывает подсказка) — следом отдельное предупреждение о противоречии, `ok = False` (решение владельца 10, H-2).
10. **Без пары**: строка остаётся в `rows`, `group_type = OTHER` (в свод не попадает, суммы не задваиваются, как #32), 10. **Без пары**: строка остаётся в `rows`, `group_type = OTHER` (в свод не попадает, суммы не задваиваются, как #32),
предупреждение «строка N: ИНН … без пары — <причина>», признак «всё склеено» = `False` → документ `needs_check`. предупреждение «строка N: ИНН … без пары — <причина>», признак «всё склеено» = `False` → документ `needs_check`.
@ -134,7 +134,7 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
| # | Дата | Решение | Обоснование | Цена ошибки | | # | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---| |---|---|---|---|---|
| 1 | 13.10.2026 | Защита «над, в дереве»: при схеме из двух и более группировок склейка только если в листе есть строки глубже схемы (проверка по листу, не по поддереву) | Контрагент, названный ИНН, с одним договором иначе потерял бы договор (договор стал бы контрагентом) | Файл с подсказкой-схемой, где 1С не добавила уровень, — строка ИНН без пары, `needs_check`; правка условия | | 1 | 13.10.2026 | Защита «над, в дереве»: при схеме из двух и более группировок склейка только если в листе есть строки глубже схемы (проверка по листу, не по поддереву) | Контрагент, названный ИНН, с одним договором иначе потерял бы договор (договор стал бы контрагентом) | При ветке глубже схемы контрагент, названный ИНН, с одним договором отдаёт ИНН договору без `needs_check` (защита работает только без подсказки и только когда лист не глубже схемы); файл с подсказкой-схемой, где 1С не добавила уровень, — строка ИНН без пары, `needs_check`; правка условия |
| 2 | 13.10.2026 | Строка ИНН без пары — `group_type = OTHER` (в `rows`, не в своде) | Как #32: суммы строки ИНН с суммами иначе задвоились бы в своде ядра; документ всё равно на проверке | Аналитик не видит такую строку контрагентом и сопоставляет вручную; поменять тип — одна строка | | 2 | 13.10.2026 | Строка ИНН без пары — `group_type = OTHER` (в `rows`, не в своде) | Как #32: суммы строки ИНН с суммами иначе задвоились бы в своде ядра; документ всё равно на проверке | Аналитик не видит такую строку контрагентом и сопоставляет вручную; поменять тип — одна строка |
| 3 | 13.10.2026 | ИНН колонки расходится с ИНН строки — побеждает колонка, `needs_check` | Колонка — явный реквизит отчёта, строка — вывод по соседству | Неверный ИНН у контрагента до проверки аналитиком; документ на проверке | | 3 | 13.10.2026 | ИНН колонки расходится с ИНН строки — побеждает колонка, `needs_check` | Колонка — явный реквизит отчёта, строка — вывод по соседству | Неверный ИНН у контрагента до проверки аналитиком; документ на проверке |
| 4 | 13.10.2026 | Направление определяется на лист ОСВ (документ), не на файл и не на счёт | Один лист — один отчёт 1С с одной настройкой вывода | Смешанные раскладки в одном листе — `needs_check` вместо склейки | | 4 | 13.10.2026 | Направление определяется на лист ОСВ (документ), не на файл и не на счёт | Один лист — один отчёт 1С с одной настройкой вывода | Смешанные раскладки в одном листе — `needs_check` вместо склейки |
@ -143,11 +143,11 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
| 7 | 13.10.2026 | Соседом считается только непосредственный сосед в списке детей того же родителя | Иначе сопоставление через чужие строки неоднозначно | ИНН через строку от наименования не склеивается, `needs_check` | | 7 | 13.10.2026 | Соседом считается только непосредственный сосед в списке детей того же родителя | Иначе сопоставление через чужие строки неоднозначно | ИНН через строку от наименования не склеивается, `needs_check` |
| 8 | 13.10.2026 | Пустая подпись — кандидат только с подсказкой и только в положении «под, в дереве» (R-1 #32) | Без подсказки пустая строка ничего не значит; другое положение у пустого ИНН в 1С не встречалось | Пустая строка ИНН «над» при подсказке — остаётся наименованием без текста | | 8 | 13.10.2026 | Пустая подпись — кандидат только с подсказкой и только в положении «под, в дереве» (R-1 #32) | Без подсказки пустая строка ничего не значит; другое положение у пустого ИНН в 1С не встречалось | Пустая строка ИНН «над» при подсказке — остаётся наименованием без текста |
| 9 | 13.10.2026 | С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью не-ИНН — нераспознанный реквизит: `OTHER`, `needs_check` | Сохраняет поведение #32 (тест «нерез.»): в 1С на этом месте стоит строка реквизита | Контрагент с одним договором с теми же суммами в файле с подсказкой «под», но без строки реквизита — `needs_check` вместо `done` | | 9 | 13.10.2026 | С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью не-ИНН — нераспознанный реквизит: `OTHER`, `needs_check` | Сохраняет поведение #32 (тест «нерез.»): в 1С на этом месте стоит строка реквизита | Контрагент с одним договором с теми же суммами в файле с подсказкой «под», но без строки реквизита — `needs_check` вместо `done` |
| 10 | 13.10.2026 | Строка ИНН с детьми под наименованием: решает подсказка; без неё — «под» (ИНН родителю, дети — договоры), если родитель по схеме `COUNTERPARTY`, иначе «над» | Иначе строка реквизита с договорами под ней (#32 со схемой «Контрагенты, Контрагенты.ИНН, Договоры» без подсказки) отдала бы ИНН договору; строка ИНН между подразделением и контрагентом — «над» | Файл без подсказки и без схемы, где ИНН под контрагентом и у строки ИНН есть дети, — ИНН уйдёт ребёнку; `needs_check` не ставится | | 10 | 13.10.2026 | Строка ИНН с детьми под наименованием: подсказка «под» — родителю (дети — договоры); иначе — родителю, если он по схеме `COUNTERPARTY` (при подсказке «над» — с подсветкой H-2, R-7, design #23), иначе ребёнку («над») | Иначе строка реквизита с договорами под ней (#32 со схемой «Контрагенты, Контрагенты.ИНН, Договоры» без подсказки) отдала бы ИНН договору; строка ИНН между подразделением и контрагентом — «над» | Файл без подсказки и без схемы, где ИНН под контрагентом и у строки ИНН есть дети, — ИНН уйдёт ребёнку; `needs_check` не ставится |
| 11 | 13.10.2026 | Task 1 (план): `__all__` модуля `numbers.py` дополнен `inn_checksum_ok` и `row_inn`; тело `inn_checksum_ok` при 12 цифрах — два булевых локала вместо одного `and`-выражения (ruff format не принял переносы из плана) | Публичные имена модуля экспортируются по соглашению файла; результат проверки не меняется | Нет: тест `test_inn_checksum_ok` покрывает оба разряда | | 11 | 13.10.2026 | Task 1 (план): `__all__` модуля `numbers.py` дополнен `inn_checksum_ok` и `row_inn`; тело `inn_checksum_ok` при 12 цифрах — два булевых локала вместо одного `and`-выражения (ruff format не принял переносы из плана) | Публичные имена модуля экспортируются по соглашению файла; результат проверки не меняется | Нет: тест `test_inn_checksum_ok` покрывает оба разряда |
| 12 | 13.10.2026 | `inn_checksum_ok`: проверка «только цифры» через `isascii() and isdigit()`, не `isdecimal()` (ревью T1.2) | `isdecimal()` пропускает не-ASCII десятичные цифры, а ИНН — только `[0-9]`; `isdigit()` без `isascii()` пропускает «²» | Нет: ИНН с не-ASCII цифрами не встречаются; такая строка даёт `False` и не становится ИНН | | 12 | 13.10.2026 | `inn_checksum_ok`: проверка «только цифры» через `isascii() and isdigit()`, не `isdecimal()` (ревью T1.2) | `isdecimal()` пропускает не-ASCII десятичные цифры, а ИНН — только `[0-9]`; `isdigit()` без `isascii()` пропускает «²» | Нет: ИНН с не-ASCII цифрами не встречаются; такая строка даёт `False` и не становится ИНН |
| 13 | 13.10.2026 | В `classify.py` локальная переменная `row_data_type` в `_classify_row` переименована в `data_type` (при публичной функции `row_data_type` иначе имя стало бы локальным и вызов упал с `UnboundLocalError`) | Переименование локальной переменной, поведение не меняется | Нет | | 13 | 13.10.2026 | В `classify.py` локальная переменная `row_data_type` в `_classify_row` переименована в `data_type` (при публичной функции `row_data_type` иначе имя стало бы локальным и вызов упал с `UnboundLocalError`) | Переименование локальной переменной, поведение не меняется | Нет |
| 14 | 13.10.2026 | R-1 (design #14, T2.1): «нераспознанный реквизит» при подсказке «под» — только ребёнок-лист (без детей в дереве) | Ребёнок со своими детьми — подлинное наименование со своей строкой ИНН; иначе единственный контрагент подразделения с теми же суммами терял ИНН (уходил в OTHER) | Реквизит «нерез.» с дочерними строками (в 1С не встречалось) станет наименованием, а не `needs_check` | | 14 | 13.10.2026 | R-1 (design #14, T2.1): «нераспознанный реквизит» при подсказке «под» — только ребёнок-лист (без детей в дереве); уточнено R-6 (design #22) | Ребёнок со своими детьми — подлинное наименование со своей строкой ИНН; иначе единственный контрагент подразделения с теми же суммами терял ИНН (уходил в OTHER) | Реквизит «нерез.» с дочерними строками (в 1С не встречалось) станет наименованием, а не `needs_check` |
| 15 | 13.10.2026 | R-2 (design #15, T2.2): строка ИНН без детей под наименованием — пара родителю только при подсказке «под», либо если родитель по схеме COUNTERPARTY, либо схема не позволяет сказать (позиция вне схемы); иначе — сосед (`flat`) | Без этого ИНН соседа контрагента под подразделением отдавался подразделению; тот же помощник позиции в схеме, что в №10 | Файл без подсказки с ИНН-ребёнком подразделения и схемой, не совпадающей с реальностью, — строка в соседях, при неопределённом направлении без пары и `needs_check` | | 15 | 13.10.2026 | R-2 (design #15, T2.2): строка ИНН без детей под наименованием — пара родителю только при подсказке «под», либо если родитель по схеме COUNTERPARTY, либо схема не позволяет сказать (позиция вне схемы); иначе — сосед (`flat`) | Без этого ИНН соседа контрагента под подразделением отдавался подразделению; тот же помощник позиции в схеме, что в №10 | Файл без подсказки с ИНН-ребёнком подразделения и схемой, не совпадающей с реальностью, — строка в соседях, при неопределённом направлении без пары и `needs_check` |
| 16 | 13.10.2026 | R-3 (design #16, T2.3): защита «над, в дереве» (нет лишнего уровня) применяется только без подсказки в шапке | Подсказка `<группировка>.ИНН` — прямое свидетельство дополнительного уровня 1С | Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает | | 16 | 13.10.2026 | R-3 (design #16, T2.3): защита «над, в дереве» (нет лишнего уровня) применяется только без подсказки в шапке | Подсказка `<группировка>.ИНН` — прямое свидетельство дополнительного уровня 1С | Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает |
| 17 | 13.10.2026 | Направление листа без подсказки: сведения пар по суммам и блоков объединяются; противоречие — направление неизвестно (строки ИНН без пары). Спека («первое определённое») читается так же, как design §6 | Design §6 прямо задаёт «противоречат — неизвестно»; «первое» по порядку источников дало бы склейку, которой блоки противоречат | Лист с парами по суммам против блоков — строки ИНН без пары и `needs_check` вместо склейки по сумме; правка объединения — одна функция `_pair_flat` | | 17 | 13.10.2026 | Направление листа без подсказки: сведения пар по суммам и блоков объединяются; противоречие — направление неизвестно (строки ИНН без пары). Спека («первое определённое») читается так же, как design §6 | Design §6 прямо задаёт «противоречат — неизвестно»; «первое» по порядку источников дало бы склейку, которой блоки противоречат | Лист с парами по суммам против блоков — строки ИНН без пары и `needs_check` вместо склейки по сумме; правка объединения — одна функция `_pair_flat` |
@ -155,3 +155,9 @@ Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — `../docs
| 19 | 13.10.2026 | H-2: направление фактической раскладки определяется по номерам строк пары (строка пары выше строки ИНН — ИНН «под», ниже — «над»), одинаково для пар в дереве и соседей; проверка выполняется только при подсказке и только для склеенных пар с непустым ИНН и без расхождения с колонкой ИНН | Номер строки однозначно отражает порядок на листе при любом пути поиска пары; пары без ИНН, без пары и с колонкой-победителем уже подсвечены своими предупреждениями | Нет: предупреждение лишь добавляется к уже состоявшейся склейке; результат склейки не меняется | | 19 | 13.10.2026 | H-2: направление фактической раскладки определяется по номерам строк пары (строка пары выше строки ИНН — ИНН «под», ниже — «над»), одинаково для пар в дереве и соседей; проверка выполняется только при подсказке и только для склеенных пар с непустым ИНН и без расхождения с колонкой ИНН | Номер строки однозначно отражает порядок на листе при любом пути поиска пары; пары без ИНН, без пары и с колонкой-победителем уже подсвечены своими предупреждениями | Нет: предупреждение лишь добавляется к уже состоявшейся склейке; результат склейки не меняется |
| 20 | 13.10.2026 | H-1: в s28 строка «7701234567» после классификации попадает в `rows` как контрагент (обычная строка по правилам классификатора) — тест фиксирует это «как есть» и проверяет её наличие среди контрагентов без ИНН | Спека: строка «классифицируется как обычно»; подгонки парсера нет | Нет: поведение соответствует ожиданию владельца | | 20 | 13.10.2026 | H-1: в s28 строка «7701234567» после классификации попадает в `rows` как контрагент (обычная строка по правилам классификатора) — тест фиксирует это «как есть» и проверяет её наличие среди контрагентов без ИНН | Спека: строка «классифицируется как обычно»; подгонки парсера нет | Нет: поведение соответствует ожиданию владельца |
| 21 | 13.10.2026 | Task 7: в tasks.md добавлены пункты 1.5 (H-2) и 2.2 (s28, H-1) сверх брифа; число golden в CLAUDE.md пересчитано по факту (123: 72 + 23 + 28, ранее значилось 113 = 72 + 22 + 19) | Бриф писался до H-1/H-2; счёт — по `find` | Нет: только документация | | 21 | 13.10.2026 | Task 7: в tasks.md добавлены пункты 1.5 (H-2) и 2.2 (s28, H-1) сверх брифа; число golden в CLAUDE.md пересчитано по факту (123: 72 + 23 + 28, ранее значилось 113 = 72 + 22 + 19) | Бриф писался до H-1/H-2; счёт — по `find` | Нет: только документация |
| 22 | 13.10.2026 | R-6 / I-1 (design #22): `_unrecognised_attributes` — ребёнок-реквизит только у наименования, чья позиция по схеме COUNTERPARTY; при такой позиции условие «ребёнок — лист» снимается (строка «нерез.» с договорами → OTHER, договоры остаются договорами); позиция по схеме не COUNTERPARTY — ребёнок не реквизит; позиции нет — прежнее условие «лист» | Иначе (регрессия #32) единственный контрагент подразделения с теми же суммами получал «ИНН не распознан» | Контрагент с единственным договором тех же сумм при подсказке «под» без строки реквизита — `needs_check` (как в №9) |
| 23 | 13.10.2026 | R-7 / I-2 (design #23): `_owned_by_parent`: подсказка «под» — родитель; иначе родитель, если по схеме COUNTERPARTY; подсказка «над» не уводит ИНН ребёнку-договору | По спеке пара — родитель; противоречие подсказке ловит H-2 (предупреждение, `needs_check`) вместо молчаливой потери ИНН | Подсказка «над» и реальный ИНН над контрагентом, у которого в схеме родитель — контрагент: ИНН уходит родителю с предупреждением, `needs_check` |
| 24 | 13.10.2026 | R-8 / I-3 (design #24): страховка в `osv.py` после классификации: ИНН склеен со строкой, не ставшей COUNTERPARTY, — предупреждение «ИНН … отнесён к строке M, но она не контрагент — проверьте», `needs_check` | ИНН иначе пропадает молча (подразделение/договор не хранят ИНН) | Лишнее `needs_check` на редкой раскладке; склейка не отменяется |
| 25 | 13.10.2026 | R-9 / M-1 (design #25): `_without` — новый родитель = ближайший неудалённый предок по прежнему `levels.parent`, а не пересчёт `parents()` по уровням | Пропуск уровней outline давал неверного родителя | Нет: уровни (`by_row`) считаются как прежде |
| 26 | 13.10.2026 | R-10 / T2.5 (design #26): порядок `_drop_conflicts` после `_check_sums` оставлен; формулировка спеки: «две строки ИНН с одной парой, прошедшие проверку сумм, — обе без пары» | Строка, не прошедшая суммы, пары не претендует; сохранение порядка не меняет результаты golden | Две строки ИНН с одной парой, одна из которых не прошла суммы: вторая склеивается |
| 27 | 13.10.2026 | H.1 финального ревью отклонено как недостижимое: пустой кандидат бывает только «под, в дереве» и всегда склеивается с родителем, расхождение с колонкой для него невозможно (комментарий в `_result`). H.2: тест на сочетание «расхождение с колонкой + противоречие подсказке» — одно предупреждение о расхождении | Недостижимый код не добавляется | Нет |

View File

@ -101,12 +101,14 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST
(10 или 12 цифр числом или текстом, «ИНН 7701234567», «ИНН: 7701234567 / 770101001»); числовая ячейка из 9/11 цифр (10 или 12 цифр числом или текстом, «ИНН 7701234567», «ИНН: 7701234567 / 770101001»); числовая ячейка из 9/11 цифр
дополняется нулём слева (R-24, #34). Без подсказки `<группировка>.ИНН` в шапке ИНН MUST пройти контрольную сумму ИНН дополняется нулём слева (R-24, #34). Без подсказки `<группировка>.ИНН` в шапке ИНН MUST пройти контрольную сумму ИНН
(алгоритм ФНС); строка из 10/12 цифр с неверной контрольной суммой рядом с наименованием MUST NOT считаться строкой ИНН: (алгоритм ФНС); строка из 10/12 цифр с неверной контрольной суммой рядом с наименованием MUST NOT считаться строкой ИНН:
она классифицируется как обычно, в `warnings` — «строка N похожа на ИНН …, контрольная сумма не сходится», документ — она классифицируется как обычно, в `warnings` — «строка N: похожа на ИНН X, но контрольная сумма не сходится — строка
`needs_check`. С подсказкой контрольная сумма не проверяется, а пустая подпись строки-ребёнка наименования — строка ИНН оставлена как есть», документ — `needs_check`. С подсказкой контрольная сумма не проверяется, а пустая подпись строки-ребёнка наименования — строка ИНН
«ИНН не указан» (склейка без ИНН, без `needs_check`, R-1 #32). **Наименование** — строка не счёт, не «Итого», не Вал./Кол., «ИНН не указан» (склейка без ИНН, без `needs_check`, R-1 #32). **Наименование** — строка не счёт, не «Итого», не Вал./Кол.,
не строка ИНН. С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью, которая не не строка ИНН. С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью, которая не
ИНН, — строка ИНН с нераспознанным ИНН (без пары, «ИНН не распознан»); таким ребёнком MUST быть только строка без детей ИНН, — строка ИНН с нераспознанным ИНН (без пары, «ИНН не распознан»); такой ребёнок бывает только у наименования, которое
(лист), у строки со своими детьми — подлинное наименование (design #14). по схеме — COUNTERPARTY (договоры под ним остаются договорами, design #22); при другой позиции по схеме (подразделение и
т. д.) ребёнок — подлинное наименование; если позиции по схеме нет, ребёнком MUST быть только строка без детей (лист,
design #14).
**Пара строки ИНН** (связь в дереве важнее соседства): **Пара строки ИНН** (связь в дереве важнее соседства):
- у строки ИНН есть дети → пара — её единственный ребёнок-наименование; несколько детей — пары нет. Если в - у строки ИНН есть дети → пара — её единственный ребёнок-наименование; несколько детей — пары нет. Если в
@ -114,7 +116,9 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST
подсказки `<группировка>.ИНН`, строка ИНН не склеивается и без предупреждения классифицируется по схеме (design #16); подсказки `<группировка>.ИНН`, строка ИНН не склеивается и без предупреждения классифицируется по схеме (design #16);
- родитель строки ИНН — наименование → пара — родитель; несколько строк ИНН у одного наименования — пары нет. Пара — - родитель строки ИНН — наименование → пара — родитель; несколько строк ИНН у одного наименования — пары нет. Пара —
родитель только при подсказке «под» или если родитель по схеме — COUNTERPARTY (или позиция вне схемы); иначе у строки родитель только при подсказке «под» или если родитель по схеме — COUNTERPARTY (или позиция вне схемы); иначе у строки
ИНН с детьми действует правило «есть дети», а без детей она — сосед (следующий пункт); ИНН с детьми действует правило «есть дети», а без детей она — сосед (следующий пункт). Строка ИНН с детьми под
наименованием при подсказке «над» и родителе-контрагенте по схеме остаётся у родителя (дети — договоры), а
противоречие подсказке подсвечивается (design #23);
- иначе — непосредственный сосед того же родителя: при суммах — сосед с теми же суммами (совпали оба — по направлению); - иначе — непосредственный сосед того же родителя: при суммах — сосед с теми же суммами (совпали оба — по направлению);
без сумм — сосед по направлению листа (над → следующий, под → предыдущий). без сумм — сосед по направлению листа (над → следующий, под → предыдущий).
@ -122,7 +126,7 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST
листа** — подсказка шапки (порядок `<группировка>.ИНН`), если она есть; иначе сведения пар соседей по суммам и однозначных листа** — подсказка шапки (порядок `<группировка>.ИНН`), если она есть; иначе сведения пар соседей по суммам и однозначных
блоков детей одного родителя из несопоставленных строк ИНН и наименований (начинается строкой ИНН — над; кончается строкой блоков детей одного родителя из несопоставленных строк ИНН и наименований (начинается строкой ИНН — над; кончается строкой
ИНН — под; начинается и кончается — блок сломан, его строки ИНН без пары) берутся вместе; если они противоречат друг другу ИНН — под; начинается и кончается — блок сломан, его строки ИНН без пары) берутся вместе; если они противоречат друг другу
или их нет — направление неизвестно, строки ИНН без сумм, которым оно нужно, остаются без пары. Две строки ИНН с одной парой — обе без пары. или их нет — направление неизвестно, строки ИНН без сумм, которым оно нужно, остаются без пары. Две строки ИНН с одной парой, прошедшие проверку сумм, — обе без пары (design #26).
**Склейка:** строка ИНН MUST NOT попадать в `rows`, её ИНН — наименованию (источник 2 требования «Классификация строк»), **Склейка:** строка ИНН MUST NOT попадать в `rows`, её ИНН — наименованию (источник 2 требования «Классификация строк»),
её дети переходят к наименованию, при «над, в дереве» наименование с поддеревом поднимается на её место; в `warnings` — её дети переходят к наименованию, при «над, в дереве» наименование с поддеревом поднимается на её место; в `warnings` —
@ -136,6 +140,9 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST
(«<группировка>.ИНН») указывает <над/под> — проверьте»; документ — `needs_check`. Для соседей без сумм направление берётся («<группировка>.ИНН») указывает <над/под> — проверьте»; документ — `needs_check`. Для соседей без сумм направление берётся
из подсказки, конфликта нет. Раскладка, совпадающая с подсказкой (в том числе «под, в дереве» в 1С), предупреждения не даёт. из подсказки, конфликта нет. Раскладка, совпадающая с подсказкой (в том числе «под, в дереве» в 1С), предупреждения не даёт.
**Страховка после классификации:** ИНН склеен со строкой, которая по схеме не стала `COUNTERPARTY`, — в `warnings` «строка N:
ИНН … отнесён к строке M, но она не контрагент — проверьте», документ — `needs_check` (design #24).
**Без пары:** строка ИНН остаётся в `rows` с `group_type = OTHER` (в свод ядра не попадает), в `warnings` — «строка N: **Без пары:** строка ИНН остаётся в `rows` с `group_type = OTHER` (в свод ядра не попадает), в `warnings` — «строка N:
ИНН … без пары — <причина>», документ — `needs_check`. Структура под склейку MUST NOT подгоняться. ИНН … без пары — <причина>», документ — `needs_check`. Структура под склейку MUST NOT подгоняться.