28 KiB
Context
Issue RSHB/arm-tabular-parser#35 (входит #34). Правило — ../docs/context/05-domain-rules.md §1.1 (реквизит
<группировка>.ИНН, OQ-162) и §1.3 (ИНН контрагента: колонка → строка ИНН → хвост названия; OQ-100, OQ-130); новое правило
заносится в §1.3 и новым OQ в 07-open-questions.md.
Восемь раскладок (ИНН над / под наименованием) × (в дереве / не в дереве) × (с суммами / без сумм). Поведение main на синтетике без подсказки в шапке:
| Раскладка | main |
|---|---|
| над, не в дереве, без сумм | склеивается (OQ-130) |
| над, не в дереве, с суммами | ИНН — отдельный контрагент, needs_check |
| над, в дереве, с суммами | ИНН вместо наименования, done |
| над, в дереве, без сумм | needs_check |
| под, в дереве, с суммами | ИНН теряется, done (с подсказкой в шапке — склеивается, #32) |
| под, в дереве, без сумм | needs_check |
| под, не в дереве, с суммами | ИНН — отдельный контрагент, needs_check |
| под, не в дереве, без сумм | ИНН первого контрагента у второго, done |
Проверено при проектировании: стратегии иерархии outline, indent, account_pattern ставят строку ИНН (с суммами и
без) на тот же уровень, что и наименования (OQ-130 отключался), то есть дерево до склейки корректно; стратегия sums не
проверялась. Контрольная сумма ИНН на golden: реальные ИНН (≈ 5 852) верны, кроме трёх (физлица: Цимлянский, ОСВ 76,
611801003692; Новопетровская 233370449085; НПЗ 614002279560), вся синтетика (5001234567, 7701234567,
7812345678 и др.) неверна.
Decisions
Решения владельца в brainstorming 13.10.2026:
- Подход A — один проход по дереву. Новый модуль
api/src/parser/inn_rows.pyработает междуresolve_levelsиclassify_rows. Он заменяет_glue_inn_rows(OQ-130, до иерархии) иglue_inn_attributes(#32, после классификации). Классификатор получает дерево без строк ИНН и ИНН для строк-наименований. - Контрольная сумма. Без подсказки в шапке строка — строка ИНН только при верной контрольной сумме ИНН (алгоритм ФНС:
10 цифр — весовые коэффициенты 2, 4, 10, 3, 5, 9, 4, 6, 8; 12 цифр — два контрольных разряда, 7, 2, 4, 10, 3, 5, 9, 4,
6, 8 и 3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8), во всех восьми раскладках, включая прежнюю OQ-130. С подсказкой
<группировка>.ИННконтрольная сумма не проверяется (файл банка #32 не меняется). - Неверная контрольная сумма. Строка из 10/12 цифр (по
inn_only_row) рядом с наименованием, не прошедшая контрольную сумму без подсказки, — не строка ИНН: классифицируется как обычно, предупреждение «строка N похожа на ИНН …, контрольная сумма не сходится», документneeds_check. - Ведущий ноль (#34). Числовая ячейка строки ИНН из 9/11 цифр дополняется нулём слева (как R-24 для колонки), контрольная сумма проверяется после дополнения.
- «Над, в дереве» — склейка только с единственным ребёнком; несколько детей — не склеивать,
needs_check. - Направление для соседей (предложено в дизайне, не оспорено): пары с суммами — по совпадению сумм; без сумм — по
направлению документа: порядок в шапке → пары по суммам → однозначные блоки. Противоречие или нет однозначных блоков —
не склеивать,
needs_check. - «Под, в дереве» — несколько строк ИНН под одним наименованием —
needs_check(как #32). - Строка ИНН — только ИНН: 10/12 цифр числом или текстом, «ИНН …», «ИНН/КПП» (
inn_only_row); счёт строкой ИНН не бывает. - Синтетика для отказа «неверная контрольная сумма» (решение владельца 13.10.2026, H-1): «синтетика для отказа
«неверная контрольная сумма» — да». Файл
s28_inn_bad_checksum.xlsx: раскладкаs20(«над, не в дереве, без сумм»), у ООО Альфа строка ИНН7701234567; ожидание — строка остаётся обычной, ИНН у Альфы нет, предупреждение «похожа на ИНН»,needs_check, остальные контрагенты получают свои ИНН (направление — по блоку 60.02). - Противоречие раскладки и подсказки шапки (решение владельца 13.10.2026, H-2): «Противоречие раскладки и подсказки
шапки — СКЛЕИВАТЬ, НО ПОДСВЕЧИВАТЬ». Пара найдена, но ИНН стоит не там, где указывает подсказка
<группировка>.ИНН(в дереве или соседом по суммам) — склейка как обычно, отдельное предупреждение «строка N: ИНН … стоит <под/над> наименованием в строке M, а шапка («…») указывает <над/под> — проверьте»,ok = False,needs_check. Закрывает T2.12.
Алгоритм inn_rows.py
Вход: строки BodyRow, Levels (уровень и родитель), подсказка шапки (InnHint: есть ли <группировка>.ИНН и
направление по порядку элементов), схема группировок group_schema (для защиты, п. «Защита» ниже), словари. Выход: строки и
Levels без склеенных строк ИНН, ИНН для строк-наименований (row → inn), множество строк ИНН без пары (классификатор
ставит им OTHER), предупреждения, признак «всё склеено».
- Кандидаты. Строка ИНН — не счёт, не «Итого», не строка Вал./Кол.; подпись совпадает с
inn_only_row(числовая ячейка из 9/11 цифр — с ведущим нулём); без подсказки ИНН проходит контрольную сумму (иначе п. 3 решений). С подсказкой пустая подпись строки-ребёнка наименования (только положение «под, в дереве») — тоже кандидат, «ИНН не указан» (R-1 #32). - Наименование — строка не счёт, не «Итого», не Вал./Кол., не кандидат.
- Положение кандидата (по
Levels):- есть дети → над, в дереве: пара — единственный ребёнок-наименование; он и его поддерево поднимаются на уровень вверх, родителем становится родитель кандидата;
- родитель — наименование → под, в дереве: пара — родитель; дети кандидата переходят к родителю;
- иначе (родитель — счёт или корень, детей нет) → не в дереве: пара — сосед того же родителя, непосредственно выше или ниже. Связь в дереве важнее соседства.
- Суммы. Если у кандидата есть суммы (
_has_sums), шесть значений MUST совпасть с суммами пары (None= 0); иначе пары нет. - Соседи с суммами: пара — сосед-наименование с равными суммами; совпали оба соседа — по направлению документа.
- Направление документа (лист ОСВ): подсказка шапки (п. 1), если она есть; иначе сведения пп. 2–3 вместе:
- подсказка шапки:
<группировка>.ИННпосле группировки — под; перед — над; - пары соседей, найденные по суммам (п. 5, один совпавший сосед);
- блоки: последовательность детей одного родителя из кандидатов «не в дереве» (I) и наименований (N), ещё не сопоставленных; начинается с I и не кончается I — над; кончается I и не начинается I — под; начинается и кончается I — блок сломан (его кандидаты без пары); ни то ни другое — нет сведений. Сведения пп. 2–3 противоречат друг другу или их нет — направление неизвестно.
- подсказка шапки:
- Соседи без сумм: пара — соседнее наименование по направлению (над → следующий сосед, под → предыдущий). Направление неизвестно — пары нет.
- Конфликты: два кандидата претендуют на одно наименование — оба без пары. ИНН в колонке ИНН у наименования отличается
от ИНН строки — колонка побеждает, предупреждение,
needs_check. ИНН в хвосте названия — строка ИНН побеждает (прежний порядок §1.3). - Склейка: строка ИНН уходит из строк и
Levels, ИНН — наименованию, предупреждение «строка N: ИНН … отнесён к контрагенту в строке M» (пустой при подсказке — «ИНН пуст … без ИНН»). Если раскладка противоречит подсказке шапки (пара выше/ниже, чем указывает подсказка) — следом отдельное предупреждение о противоречии,ok = False(решение 10). - Без пары: строка остаётся в
rows,group_type = OTHER(в свод не попадает, суммы не задваиваются, как #32), предупреждение «строка N: ИНН … без пары — <причина>», признак «всё склеено» =False→ документneeds_check.
Защита «над, в дереве». Контрагент, названный ИНН, с одним договором неотличим по форме от строки ИНН над
контрагентом. Проверка — по листу: если group_schema содержит больше одной группировки и ни одна строка листа (кроме
Вал./Кол.) не стоит глубже схемы (позиция — число не-счётных предков после последнего счёта + 1 — не больше
len(group_schema) − 1), лишнего уровня в листе нет: кандидаты «над, в дереве» не склеиваются, без предупреждения
классифицируются по схеме. Схема из одного ACCOUNT — проверки нет, по пп. 1–10.
Нераспознанный реквизит при подсказке «под». С подсказкой «под» (#32) единственный ребёнок наименования с теми же
суммами, подпись которого не ИНН и не пуста («нерез.»), — строка реквизита с нераспознанным ИНН: без пары, OTHER,
«ИНН не распознан», needs_check (поведение #32 сохраняется).
Изменения в коде
inn_rows.py— новый модуль (пп. 1–10); контрольная сумма ИНН —numbers.py(inn_checksum_ok).header.py:InnAttributeи позиции-реквизиты удаляются;GroupSchemaполучает подсказку (inn_hint: владелец и направление);<группировка>.ИННраспознаётся и перед группировкой-владельцем (сейчас только после).classify.py: удаляются_glue_inn_rows,glue_inn_attributes,_not_glued_reason, поляis_inn_attribute,attribute_inn;_classify_rowберёт ИНН строки из результатаinn_rowsвместоinn_aboveи ставитOTHERстрокам без пары.hierarchy.py: полеBodyRow.inn_aboveудаляется.osv.py: вызовinn_rowsмеждуresolve_levelsиclassify_rows; признак «всё склеено» и предупреждения — в статус иwarnings.
Тесты
- Юнит-тесты
inn_rows.pyна деревьях вручную: распознавание (10/12, число/текст, «ИНН …», «ИНН/КПП», ведущий ноль), контрольная сумма с подсказкой и без, все отказы пп. 4–8 и 10, защита «над, в дереве», направление (шапка, суммы, блоки, противоречие, нет сведений, сломанный блок), расхождение с колонкой, R-1. - Синтетика
s20…s27(по одной на раскладку, ИНН с верной контрольной суммой, шапка без подсказки), golden — после ручной проверки: ИНН у всех контрагентов, итоги сходятся,done. s18: ИНН с верной контрольной суммой (только вs18, общие узлы генератора не меняются).- Сравнение результатов main и ветки на всех файлах fixtures и синтетике; ожидаемые отличия —
s18, новыеs20…s27, файлы со строкой ИНН с неверной контрольной суммой; Новопетровская без изменений. Каждое отличие согласуется с владельцем доmake golden-update.
Decisions made without discussion
| # | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 13.10.2026 | Защита «над, в дереве»: при схеме из двух и более группировок склейка только если в листе есть строки глубже схемы (проверка по листу, не по поддереву) | Контрагент, названный ИНН, с одним договором иначе потерял бы договор (договор стал бы контрагентом) | Файл с подсказкой-схемой, где 1С не добавила уровень, — строка ИНН без пары, needs_check; правка условия |
| 2 | 13.10.2026 | Строка ИНН без пары — group_type = OTHER (в rows, не в своде) |
Как #32: суммы строки ИНН с суммами иначе задвоились бы в своде ядра; документ всё равно на проверке | Аналитик не видит такую строку контрагентом и сопоставляет вручную; поменять тип — одна строка |
| 3 | 13.10.2026 | ИНН колонки расходится с ИНН строки — побеждает колонка, needs_check |
Колонка — явный реквизит отчёта, строка — вывод по соседству | Неверный ИНН у контрагента до проверки аналитиком; документ на проверке |
| 4 | 13.10.2026 | Направление определяется на лист ОСВ (документ), не на файл и не на счёт | Один лист — один отчёт 1С с одной настройкой вывода | Смешанные раскладки в одном листе — needs_check вместо склейки |
| 5 | 13.10.2026 | Подсказка шапки задаёт направление сильнее пар по суммам; пары по суммам склеиваются независимо от направления | Суммы однозначно указывают пару; шапка — явная настройка отчёта | Противоречие шапки и сумм не поднимается как ошибка |
| 6 | 13.10.2026 | Суммы сравниваются с None = 0 (в #32 — точное равенство кортежей) |
Пустая ячейка и 0 в ОСВ 1С значат одно и то же | Пара по суммам у строк, различающихся только пустотой ячейки; на Новопетровской не влияет (проверяется golden) |
| 7 | 13.10.2026 | Соседом считается только непосредственный сосед в списке детей того же родителя | Иначе сопоставление через чужие строки неоднозначно | ИНН через строку от наименования не склеивается, needs_check |
| 8 | 13.10.2026 | Пустая подпись — кандидат только с подсказкой и только в положении «под, в дереве» (R-1 #32) | Без подсказки пустая строка ничего не значит; другое положение у пустого ИНН в 1С не встречалось | Пустая строка ИНН «над» при подсказке — остаётся наименованием без текста |
| 9 | 13.10.2026 | С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью не-ИНН — нераспознанный реквизит: OTHER, needs_check |
Сохраняет поведение #32 (тест «нерез.»): в 1С на этом месте стоит строка реквизита | Контрагент с одним договором с теми же суммами в файле с подсказкой «под», но без строки реквизита — needs_check вместо done |
| 10 | 13.10.2026 | Строка ИНН с детьми под наименованием: решает подсказка; без неё — «под» (ИНН родителю, дети — договоры), если родитель по схеме COUNTERPARTY, иначе «над» |
Иначе строка реквизита с договорами под ней (#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 покрывает оба разряда |
| 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) |
Переименование локальной переменной, поведение не меняется | Нет |
| 14 | 13.10.2026 | R-1 (design #14, T2.1): «нераспознанный реквизит» при подсказке «под» — только ребёнок-лист (без детей в дереве) | Ребёнок со своими детьми — подлинное наименование со своей строкой ИНН; иначе единственный контрагент подразделения с теми же суммами терял ИНН (уходил в OTHER) | Реквизит «нерез.» с дочерними строками (в 1С не встречалось) станет наименованием, а не 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С |
Подсказка «над» в шапке при реально отсутствующем уровне: контрагент, названный ИНН, склеится с ребёнком; документ — на проверку не попадает |
| 17 | 13.10.2026 | Направление листа без подсказки: сведения пар по суммам и блоков объединяются; противоречие — направление неизвестно (строки ИНН без пары). Спека («первое определённое») читается так же, как design §6 | Design §6 прямо задаёт «противоречат — неизвестно»; «первое» по порядку источников дало бы склейку, которой блоки противоречат | Лист с парами по суммам против блоков — строки ИНН без пары и needs_check вместо склейки по сумме; правка объединения — одна функция _pair_flat |
| 18 | 13.10.2026 | Task 4: параметризованный test_header_status_and_hierarchy[s18_inn_row_above.xlsx] (ожидает done) и golden s18 не правятся в Task 4 — оба падают только из-за ИНН s18 с неверной контрольной суммой и чинятся вместе с файлом в Task 5–6; в коммите упавшие тесты выведены из прогона (--deselect), test_s18_… помечен strict xfail |
Бриф Task 4 прямо запрещает обновлять golden; ожидание done в параметризации относится к тому же файлу |
Если Task 5 забудет снять xfail/починить параметризацию, strict xfail упадёт (XPASS) и напомнит |
| 19 | 13.10.2026 | H-2: направление фактической раскладки определяется по номерам строк пары (строка пары выше строки ИНН — ИНН «под», ниже — «над»), одинаково для пар в дереве и соседей; проверка выполняется только при подсказке и только для склеенных пар с непустым ИНН и без расхождения с колонкой ИНН | Номер строки однозначно отражает порядок на листе при любом пути поиска пары; пары без ИНН, без пары и с колонкой-победителем уже подсвечены своими предупреждениями | Нет: предупреждение лишь добавляется к уже состоявшейся склейке; результат склейки не меняется |
| 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 |
Нет: только документация |