From a1c6c10869d13505fa581445b172540aaf16e2a2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 14:02:44 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BF=D0=B5=D0=BA=D0=B0:=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D0=B8=20=D0=98=D0=9D=D0=9D=20=D1=80=D1=8F?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=20=D1=81=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=BE=D0=BC=20=E2=80=94=208=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D0=BA=D0=BB=D0=B0=D0=B4=D0=BE=D0=BA=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../changes/osv-inn-rows-layouts/design.md | 129 ++++++++++++++ .../changes/osv-inn-rows-layouts/proposal.md | 61 +++++++ .../specs/osv-parsing/spec.md | 165 ++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 openspec/changes/osv-inn-rows-layouts/design.md create mode 100644 openspec/changes/osv-inn-rows-layouts/proposal.md create mode 100644 openspec/changes/osv-inn-rows-layouts/specs/osv-parsing/spec.md diff --git a/openspec/changes/osv-inn-rows-layouts/design.md b/openspec/changes/osv-inn-rows-layouts/design.md new file mode 100644 index 0000000..b4a7d39 --- /dev/null +++ b/openspec/changes/osv-inn-rows-layouts/design.md @@ -0,0 +1,129 @@ +## 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: + +1. **Подход A — один проход по дереву.** Новый модуль `api/src/parser/inn_rows.py` работает между `resolve_levels` и + `classify_rows`. Он заменяет `_glue_inn_rows` (OQ-130, до иерархии) и `glue_inn_attributes` (#32, после + классификации). Классификатор получает дерево без строк ИНН и ИНН для строк-наименований. +2. **Контрольная сумма.** Без подсказки в шапке строка — строка ИНН только при верной контрольной сумме ИНН (алгоритм ФНС: + 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 не меняется). +3. **Неверная контрольная сумма.** Строка из 10/12 цифр (по `inn_only_row`) рядом с наименованием, не прошедшая + контрольную сумму без подсказки, — не строка ИНН: классифицируется как обычно, предупреждение «строка N похожа на ИНН + …, контрольная сумма не сходится», документ `needs_check`. +4. **Ведущий ноль (#34).** Числовая ячейка строки ИНН из 9/11 цифр дополняется нулём слева (как R-24 для колонки), + контрольная сумма проверяется после дополнения. +5. **«Над, в дереве»** — склейка только с единственным ребёнком; несколько детей — не склеивать, `needs_check`. +6. **Направление для соседей** (предложено в дизайне, не оспорено): пары с суммами — по совпадению сумм; без сумм — по + направлению документа: порядок в шапке → пары по суммам → однозначные блоки. Противоречие или нет однозначных блоков — + не склеивать, `needs_check`. +7. **«Под, в дереве»** — несколько строк ИНН под одним наименованием — `needs_check` (как #32). +8. **Строка ИНН — только ИНН**: 10/12 цифр числом или текстом, «ИНН …», «ИНН/КПП» (`inn_only_row`); счёт строкой ИНН не + бывает. + +### Алгоритм `inn_rows.py` + +Вход: строки `BodyRow`, `Levels` (уровень и родитель), подсказка шапки (`InnHint`: есть ли `<группировка>.ИНН` и +направление по порядку элементов), позиции схемы группировок (для защиты, п. «Защита» ниже), словари. Выход: строки и +`Levels` без склеенных строк ИНН, ИНН для строк-наименований (`row → inn`), множество строк ИНН без пары (классификатор +ставит им `OTHER`), предупреждения, признак «всё склеено». + +1. **Кандидаты.** Строка ИНН — не счёт, не «Итого», не строка Вал./Кол.; подпись совпадает с `inn_only_row` (числовая + ячейка из 9/11 цифр — с ведущим нулём); без подсказки ИНН проходит контрольную сумму (иначе п. 3 решений). С + подсказкой пустая подпись строки-ребёнка наименования (только положение «под, в дереве») — тоже кандидат, «ИНН не + указан» (R-1 #32). +2. **Наименование** — строка не счёт, не «Итого», не Вал./Кол., не кандидат. +3. **Положение кандидата** (по `Levels`): + - есть дети → **над, в дереве**: пара — единственный ребёнок-наименование; он и его поддерево поднимаются на уровень + вверх, родителем становится родитель кандидата; + - родитель — наименование → **под, в дереве**: пара — родитель; дети кандидата переходят к родителю; + - иначе (родитель — счёт или корень, детей нет) → **не в дереве**: пара — сосед того же родителя, непосредственно + выше или ниже. + Связь в дереве важнее соседства. +4. **Суммы.** Если у кандидата есть суммы (`_has_sums`), шесть значений MUST совпасть с суммами пары (`None` = 0); иначе + пары нет. +5. **Соседи с суммами**: пара — сосед-наименование с равными суммами; совпали оба соседа — по направлению документа. +6. **Направление документа** (лист ОСВ), первое определённое: + 1. подсказка шапки: `<группировка>.ИНН` после группировки — под; перед — над; + 2. пары соседей, найденные по суммам (п. 5, один совпавший сосед); + 3. блоки: последовательность детей одного родителя из кандидатов «не в дереве» (I) и наименований (N), ещё не + сопоставленных; начинается с I и не кончается I — над; кончается I и не начинается I — под; начинается и кончается I + — блок сломан (его кандидаты без пары); ни то ни другое — нет сведений. + Сведения пп. 2–3 противоречат друг другу или их нет — направление неизвестно. +7. **Соседи без сумм**: пара — соседнее наименование по направлению (над → следующий сосед, под → предыдущий). + Направление неизвестно — пары нет. +8. **Конфликты**: два кандидата претендуют на одно наименование — оба без пары. ИНН в колонке ИНН у наименования отличается + от ИНН строки — колонка побеждает, предупреждение, `needs_check`. ИНН в хвосте названия — строка ИНН побеждает + (прежний порядок §1.3). +9. **Склейка**: строка ИНН уходит из строк и `Levels`, ИНН — наименованию, предупреждение «строка N: ИНН … отнесён к + контрагенту в строке M» (пустой при подсказке — «ИНН пуст … без ИНН»). +10. **Без пары**: строка остаётся в `rows`, `group_type = OTHER` (в свод не попадает, суммы не задваиваются, как #32), + предупреждение «строка N: ИНН … без пары — <причина>», признак «всё склеено» = `False` → документ `needs_check`. + +**Защита «над, в дереве».** Контрагент, названный ИНН, с одним договором неотличим по форме от строки ИНН над +контрагентом. Если шапка задаёт группировки (позиции схемы) и глубина поддерева кандидата не превышает схему (лишнего +уровня нет), кандидат не склеивается и классифицируется по схеме; без схемы — по пп. 1–10. + +### Изменения в коде + +- `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С не встречалось | Пустая строка ИНН «над» при подсказке — остаётся наименованием без текста | diff --git a/openspec/changes/osv-inn-rows-layouts/proposal.md b/openspec/changes/osv-inn-rows-layouts/proposal.md new file mode 100644 index 0000000..b59c9d9 --- /dev/null +++ b/openspec/changes/osv-inn-rows-layouts/proposal.md @@ -0,0 +1,61 @@ +## Why + +Требование владельца (13.10.2026, RSHB/arm-tabular-parser#35): ОСВ, где ИНН контрагента выведен **отдельной строкой** +рядом с наименованием, должна разбираться во всех восьми раскладках: (ИНН над / под наименованием) × (в дереве / не в +дереве) × (с суммами / без сумм). Сейчас парсер знает две частные раскладки: «над, не в дереве, без сумм» (OQ-130, +`_glue_inn_rows`) и «под, в дереве, с суммами» только при подсказке «Контрагенты.ИНН» в шапке (OQ-162, #32, +`glue_inn_attributes`). Остальные шесть дают задвоение контрагентов, ИНН вместо наименования или потерю ИНН, а «под, не в +дереве, без сумм» — **дефект main**: ИНН первого контрагента молча достаётся второму, документ `done`. + +Входит RSHB/arm-tabular-parser#34: числовая ячейка строки ИНН из 9/11 цифр (потерянный ведущий ноль, R-24) не +распознаётся как ИНН. + +Срез: доработка Среза 2a (парсер ОСВ, `09-delivery-plan.md`). Репозитории: tabular-parser (код и эта спека), arm-ka-docs +(`05-domain-rules.md` §1.1, §1.3, новый OQ), arm-ka-fixtures (синтетика и golden). Таблицы DBML не затрагиваются +(`osv_entry.counterparty_inn` заполняет ядро из контракта, контракт не меняется). + +## What Changes + +- Новый модуль `api/src/parser/inn_rows.py`: один проход по дереву строк между построением иерархии и классификацией. + Находит строки ИНН, сопоставляет каждую с её контрагентом, убирает склеенную строку из дерева (дети переходят к + контрагенту), отдаёт классификатору чистое дерево и ИНН для строк контрагентов. +- Строка ИНН распознаётся по содержимому (`inn_only_row` в `osv_headers.yaml`, 10/12 цифр; число из 9/11 цифр — с + ведущим нулём, #34). Без подсказки в шапке ИНН MUST пройти проверку контрольной суммы; строка из 10/12 цифр рядом с + контрагентом с неверной контрольной суммой — не строка ИНН, предупреждение и `needs_check`. +- Подсказка в шапке — элемент `<группировка>.ИНН` (OQ-162): снимает проверку контрольной суммы и задаёт направление + порядком («Контрагенты, Контрагенты.ИНН» — под; «Контрагенты.ИНН, Контрагенты» — над; второй порядок сейчас не + распознаётся). +- Сопоставление: связь в дереве (строка ИНН — родитель единственного ребёнка или ребёнок наименования) важнее соседства; + соседи — по равенству сумм или по направлению документа (шапка → пары по суммам → однозначные блоки). Неоднозначность + — строка не склеивается, `group_type = OTHER`, предупреждение с причиной, `needs_check`. +- Удаляются `_glue_inn_rows` (OQ-130), `glue_inn_attributes` и тип `InnAttribute` в схеме классификатора (#32); поле + `BodyRow.inn_above` заменяется ИНН из соседней строки. +- Синтетика `s20`…`s27` (по одной на раскладку); `s18` получает ИНН с верной контрольной суммой. +- Требование «Классификация строк» спеки `osv-parsing` переписывается целиком: текущая версия устарела («ИНН только из + колонки», R-17) и не отражает OQ-100, OQ-130, OQ-162. + +## Non-goals + +- Контракт результата (`counterparty_inn`, `rows`, `warnings`) не меняется, `CONTRACT_VERSION` прежний. +- ИНН в тексте названия (OQ-100) и в колонке ИНН (R-15, R-24, «ИНН/КПП») — прежние правила, порядок приоритета прежний: + колонка → строка ИНН → хвост названия. +- Проверка контрольной суммы ИНН из колонки и из хвоста названия не вводится. +- Реквизиты, кроме ИНН (`Контрагенты.КПП` и др.), строками не склеиваются. +- Follow-ups #36 (флейк интеграционного теста O-1, дубли `extra` O-2) — не здесь. + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- `osv-parsing`: требование «Шапка документа и схема группировок» (подсказка `<группировка>.ИНН` в обоих порядках); + требование «Классификация строк» (переписано целиком: ИНН из колонки, строки ИНН, хвоста названия); новое требование + «Строки ИНН рядом с контрагентом». + +## Impact + +- Документы с ИНН строкой, у которых контрольная сумма ИНН неверна и подсказки в шапке нет, переходят из `done` в + `needs_check` (решение владельца). Какие реальные файлы fixtures затронуты, покажет сравнение main и ветки на всех + файлах; каждое отличие golden согласуется с владельцем до `make golden-update`. +- Файл банка Новопетровской (#32, подсказка в шапке) MUST NOT измениться. +- После выкладки: пересобрать контейнер парсера на стенде; в ядре правок нет. diff --git a/openspec/changes/osv-inn-rows-layouts/specs/osv-parsing/spec.md b/openspec/changes/osv-inn-rows-layouts/specs/osv-parsing/spec.md new file mode 100644 index 0000000..4db1d3c --- /dev/null +++ b/openspec/changes/osv-inn-rows-layouts/specs/osv-parsing/spec.md @@ -0,0 +1,165 @@ +## MODIFIED Requirements + +### Requirement: Шапка документа и схема группировок +Парсер SHALL извлечь из области выше заголовка таблицы: `organization_name` (первая непустая текстовая строка, trim), +`account_code_raw` (после «по счету»/«по счёту», как в файле: «60», «58.03»), `period_text` (после «за» до «г.»/конца), +`data_type` («Выводимые данные: БУ/НУ/сумма»), `currency` («Валюта …: RUB/руб.» → `RUB`), `header_variant` (1 — группировки +строками первой колонки блока заголовка; 2 — перечисление «субконто: …»). `group_schema` SHALL начинаться с ACCOUNT и далее +перечислять группировки в порядке вложенности по словарю `rules/osv_groups.yaml`; ячейка `ИНН` в строке группировки → +`has_inn = true`. Элемент вида `<группировка>.ИНН` (суффикс после последней точки — по словарю `service_columns.inn`, +префикс — имя группировки `COUNTERPARTY` этой же шапки; вариант 2 — элемент списка «субконто: …», вариант 1 — строка блока +заголовка) MUST NOT попадать в `group_schema`: группировка-владелец получает `has_inn = true`, а элемент служит +**подсказкой** для строк ИНН (требование «Строки ИНН рядом с контрагентом»): стоит после владельца — ИНН под +наименованием, перед владельцем — над. `period_quarter`/`is_cumulative` SHALL вычисляться только по тексту по +`rules/periods.yaml` (§1.5, `ASSUMPTION(OQ-02)`), `period_year` — четыре цифры года. Отсутствие любого поля шапки, кроме +обязательных по контракту, — `null` и предупреждение; отсутствие `organization_name`/`period_text` — пустая строка и +предупреждение (контракт требует строку). + +#### Scenario: Вариант 1 с тремя строками группировок +- **WHEN** разбирается `schastl/2024-Q3/…62 за 3 квартал 2024 г..xls` +- **THEN** `group_schema = [ACCOUNT, DEPARTMENT, COUNTERPARTY(has_inn=true)]`, `header_variant = 1`, + `account_code_raw = "62"`, `period_text = "3 квартал 2024 г."`, `period_quarter = "Q3"`, `is_cumulative = false`, + `data_type = "BU"`, `currency = "RUB"` + +#### Scenario: ОСВ по субсчёту и годовой период +- **WHEN** разбираются `rb/2024-Q3/сч 583_3 кв 2024.xlsx` и `rb/2024-FY/ОСВ_66 за 2024.xls` +- **THEN** у первого `account_code_raw = "58.03"`; у второго `period_quarter = "Q4"`, `is_cumulative = true` + +#### Scenario: Вариант 2 +- **WHEN** разбирается синтетическая ОСВ с шапкой «Период: 1 квартал 2026 г.», «Детализация по субсчетам, субконто: + Контрагенты, Договоры», заголовком «Субконто» +- **THEN** `header_variant = 2`, `group_schema = [ACCOUNT, COUNTERPARTY, CONTRACT]`, `period_quarter = "Q1"` + +#### Scenario: Реквизит ИНН после группировки +- **WHEN** разбирается `osv-dkb/novopetrovskaya/2025-Q3/60_ 9мес.2025г.xls` (шапка «субконто: Контрагенты, + Контрагенты.ИНН») +- **THEN** `group_schema = [ACCOUNT, COUNTERPARTY(has_inn=true)]`, подсказка «ИНН под наименованием» + +#### Scenario: Реквизит ИНН перед группировкой +- **WHEN** в синтетической ОСВ шапка «субконто: Контрагенты.ИНН, Контрагенты» +- **THEN** `group_schema = [ACCOUNT, COUNTERPARTY(has_inn=true)]`, подсказка «ИНН над наименованием» + +### Requirement: Классификация строк +Классификация SHALL выполняться над деревом, из которого уже убраны склеенные строки ИНН (требование «Строки ИНН рядом с +контрагентом»). Каждая строка данных SHALL получить `group_type`: ACCOUNT — текст матчит `ACCOUNT_RE` (регистр +учитывается, IGNORECASE нет): две-три цифры и до двух сегментов субсчёта через точку, где сегмент — `\d{1,2}` **или** 1–3 +заглавные буквы кириллицы/латиницы («76.АВ», «76.ВА», «76.ОТ», R-16), далее опционально запятая и название: +`^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` с флагами `re.ASCII | re.DOTALL` (после запятой допустим любой +пробел или его отсутствие, R-18; цифры — только 0–9; название после запятой может занимать несколько строк ячейки — M-1); +`account_code_raw` — код как в файле, без названия после запятой. Не-счёт — группировка из `group_schema` по позиции +(число не-счётных предков после последнего ACCOUNT + 1), при выходе за схему — OTHER; строка ИНН без пары — OTHER; +последняя строка уровня 0 с текстом из словаря «Итого» — TOTAL. + +Для COUNTERPARTY `counterparty_name` SHALL быть текстом строки без ИНН в конце и разделителя перед ним (`row_value` — +исходный текст; OQ-100), а `counterparty_inn` — первым найденным из: +1. колонки ИНН: цифры текстовой ячейки («ИНН 352819549855» → «352819549855», R-15), склеенная «ИНН/КПП» (`inn_kpp_cell`) + → ИНН; результат только при 10 или 12 цифрах, иначе `null`; число из 9/11 цифр (ИНН регионов 01–09 с потерянным + ведущим нулём) дополняется нулём слева (R-24); +2. строки ИНН, склеенной с этой строкой (требование «Строки ИНН рядом с контрагентом»); +3. конца текста названия: 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН» + (`counterparty_inn_suffix`; «Полиграф, 3528315710», «ООО Альфа (ИНН 7701234567)»). +Контрольная сумма ИНН из колонки и из названия MUST NOT проверяться. Строки «Вал.», «Кол.», трёхбуквенного кода валюты +SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST NOT занимать позицию в `group_schema` и MUST NOT +наследовать `counterparty_name`/`counterparty_inn` (`account_code_raw` наследуется от ветки, R-19); их суммы остаются в +шести полях. Остальные строки получают `data_type` шапки. Все строки, включая TOTAL, SHALL присутствовать в `rows` +(`ASSUMPTION(OQ-01)`: хранить ли «Итого», решает ядро); склеенные строки ИНН в `rows` MUST NOT попадать. + +#### Scenario: Подразделение, контрагент с ИНН, физлицо +- **WHEN** разбирается `schastl/2024-Q3/…62 за 3 квартал 2024 г..xls` +- **THEN** «БП Аренда» → DEPARTMENT; «МАЛЕЧКИНСКАЯ ПТИЦЕФАБРИКА ООО» → COUNTERPARTY с `counterparty_inn` из 10 цифр; + «Ведомость в счет з/платы» → COUNTERPARTY с `counterparty_inn = null` + +#### Scenario: ИНН в конце названия +- **WHEN** разбирается синтетическая ОСВ `s17` (ИНН в тексте названия с разными разделителями) +- **THEN** у контрагентов `counterparty_name` без ИНН, `counterparty_inn` — ИНН из текста, `row_value` — исходный текст + +#### Scenario: «ИНН/КПП» в одной ячейке колонки ИНН +- **WHEN** разбирается синтетическая ОСВ `s19` +- **THEN** `counterparty_inn` — 10 цифр до «/» + +#### Scenario: Название субсчёта в строке счёта +- **WHEN** в синтетической ОСВ строка «60.01, Расчеты с поставщиками» +- **THEN** `group_type = ACCOUNT`, `account_code_raw = "60.01"`, `row_value` — исходный текст + +#### Scenario: Буквенный субсчёт +- **WHEN** в ОСВ 76 строка «76.АВ» (реальные файлы РБ и Счастливочной) либо «60.АВ, …» (синтетика) +- **THEN** `group_type = ACCOUNT`, строка наследуется детьми как счёт ветки; «76.ав» (строчные) счётом не считается + +#### Scenario: Строки Вал./Кол. +- **WHEN** в синтетической ОСВ под контрагентом строки «USD» и «Кол.» +- **THEN** `group_type = OTHER`, `data_type = CURRENCY` и `QUANTITY`, `counterparty_name`/`counterparty_inn` равны `null`, + `account_code_raw` совпадает с родителем, строки исключены из проверки «сумма детей = родитель», статус `done` + +## ADDED Requirements + +### Requirement: Строки ИНН рядом с контрагентом +Между построением иерархии и классификацией парсер SHALL найти строки ИНН и сопоставить каждую с её строкой-наименованием +в любой из восьми раскладок: ИНН над / под наименованием × в дереве (родитель или ребёнок наименования) / не в дереве +(сосед того же родителя) × с суммами / без сумм. + +**Строка ИНН** — не счёт, не «Итого», не строка Вал./Кол., подпись которой целиком — ИНН по словарю `inn_only_row` +(10 или 12 цифр числом или текстом, «ИНН 7701234567», «ИНН: 7701234567 / 770101001»); числовая ячейка из 9/11 цифр +дополняется нулём слева (R-24, #34). Без подсказки `<группировка>.ИНН` в шапке ИНН MUST пройти контрольную сумму ИНН +(алгоритм ФНС); строка из 10/12 цифр с неверной контрольной суммой рядом с наименованием MUST NOT считаться строкой ИНН: +она классифицируется как обычно, в `warnings` — «строка N похожа на ИНН …, контрольная сумма не сходится», документ — +`needs_check`. С подсказкой контрольная сумма не проверяется, а пустая подпись строки-ребёнка наименования — строка ИНН +«ИНН не указан» (склейка без ИНН, без `needs_check`, R-1 #32). **Наименование** — строка не счёт, не «Итого», не Вал./Кол., +не строка ИНН. + +**Пара строки ИНН** (связь в дереве важнее соседства): +- у строки ИНН есть дети → пара — её единственный ребёнок-наименование; несколько детей — пары нет. Если шапка задаёт + группировки и поддерево строки ИНН не глубже схемы (лишнего уровня нет), строка ИНН не склеивается и классифицируется + по схеме; +- родитель строки ИНН — наименование → пара — родитель; несколько строк ИНН у одного наименования — пары нет; +- иначе — непосредственный сосед того же родителя: при суммах — сосед с теми же суммами (совпали оба — по направлению); + без сумм — сосед по направлению листа (над → следующий, под → предыдущий). + +Если у строки ИНН есть суммы, все шесть MUST совпасть с суммами пары (пустая ячейка = 0), иначе пары нет. **Направление +листа** — первое определённое из: порядок `<группировка>.ИНН` в шапке; пары соседей, найденные по суммам; однозначные блоки +детей одного родителя из несопоставленных строк ИНН и наименований (начинается строкой ИНН — над; кончается строкой ИНН — +под; начинается и кончается — блок сломан, его строки ИНН без пары). Противоречие сведений или их отсутствие — направление +неизвестно, строки ИНН без сумм, которым оно нужно, остаются без пары. Две строки ИНН с одной парой — обе без пары. + +**Склейка:** строка ИНН MUST NOT попадать в `rows`, её ИНН — наименованию (источник 2 требования «Классификация строк»), +её дети переходят к наименованию, при «над, в дереве» наименование с поддеревом поднимается на её место; в `warnings` — +«строка N: ИНН … отнесён к контрагенту в строке M». ИНН колонки ИНН у наименования, отличный от ИНН строки, остаётся, +в `warnings` — расхождение, документ — `needs_check`. + +**Без пары:** строка ИНН остаётся в `rows` с `group_type = OTHER` (в свод ядра не попадает), в `warnings` — «строка N: +ИНН … без пары — <причина>», документ — `needs_check`. Структура под склейку MUST NOT подгоняться. + +#### Scenario: Восемь раскладок без подсказки +- **WHEN** разбираются синтетические ОСВ `s20`…`s27` (по одной на раскладку; ИНН с верной контрольной суммой; шапка без + `Контрагенты.ИНН`) +- **THEN** у каждого контрагента `counterparty_inn` — ИНН его строки ИНН, строк ИНН в `rows` нет, контрагентов столько же, + сколько наименований, контрольные суммы сходятся, статус `done` + +#### Scenario: ИНН под наименованием не уходит следующему контрагенту +- **WHEN** в синтетической ОСВ `s27` (ИНН под наименованием, не в дереве, без сумм) идут «ООО Альфа», ИНН Альфы, + «ООО Бета», ИНН Беты +- **THEN** ИНН Альфы — у «ООО Альфа», ИНН Беты — у «ООО Бета» + +#### Scenario: ИНН над наименованием (OQ-130) +- **WHEN** разбирается синтетическая ОСВ `s18` (строка ИНН без сумм над наименованием, ИНН с верной контрольной суммой) +- **THEN** ИНН — у контрагента под строкой, строк ИНН в `rows` нет, статус `done` + +#### Scenario: Реквизит «Контрагенты.ИНН» (OQ-162) +- **WHEN** разбирается `osv-dkb/novopetrovskaya/2025-Q3/60_ 9мес.2025г.xls` +- **THEN** результат совпадает с golden до изменения: 713 из 716 контрагентов с ИНН (включая ИНН с неверной контрольной + суммой), 3 физлица без ИНН, статус `done` + +#### Scenario: Неверная контрольная сумма без подсказки +- **WHEN** в синтетической ОСВ без подсказки над наименованием строка «7701234567» (контрольная сумма не сходится) +- **THEN** строка не склеивается, в `warnings` — «похожа на ИНН, контрольная сумма не сходится», статус `needs_check` + +#### Scenario: Ведущий ноль у числовой строки ИНН +- **WHEN** строка ИНН без подсказки — числовая ячейка `123456788` (9 цифр; `0123456788` проходит контрольную сумму) +- **THEN** `counterparty_inn = "0123456788"` + +#### Scenario: Неоднозначное направление +- **WHEN** под счётом блок «наименование, ИНН без сумм, наименование» и других сведений о направлении нет +- **THEN** строка ИНН без пары, `group_type = OTHER`, предупреждение «направление неизвестно», статус `needs_check` + +#### Scenario: Несколько детей у строки ИНН +- **WHEN** у строки ИНН в дереве два ребёнка-наименования +- **THEN** строка ИНН без пары, `group_type = OTHER`, статус `needs_check`