Спеки применены: 5 новых capability (arm-core, arm-web, scan-gateway, statement-normalization, tabular-recognition-service) с Purpose, изменения quality-checks и recognition-contract. Ссылки в 05-domain-rules, DBML v5.19, entities, dev/checks.md и dev/qa-statements-v2.md ведут в архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
917 lines
98 KiB
Markdown
917 lines
98 KiB
Markdown
# statement-normalization Specification
|
||
|
||
## Purpose
|
||
Общий алгоритм нормализации форм ББ/ОФР (шаги 2–8) в пакете `arm_statements`, одинаковый для tabular-parser, scan-gateway и пересчёта в arm-core: колонки кодов и периодов, статусы строк, разбор сумм, код и редакция формы, проверки §7, признак «требует внимания», предупреждения и статус документа; копирование пакета в хосты скриптом sync-statements.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Пакет общего кода `arm_statements` и его копии
|
||
Общий код нормализации ББ/ОФР SHALL жить в репозитории `arm-statements` пакетом `arm_statements/` с `pyproject`,
|
||
своими тестами и конфигом линтеров. Пакет SHALL содержать: типы шага 1 (`FormCandidate`, `Cell`; сериализуемые в
|
||
json dataclass-ы), шаги 2–8, общие разборы текста (двойники, даты, суммы, коды), словари — проверок §7
|
||
`statement_checks.yaml` (полный и упрощённый наборы), периодов `statement_periods.yaml`, заголовков форм и ОКУД,
|
||
подписей колонок («Код», «Наименование», исключения), букв-двойников, единиц измерения `statement_units.yaml` (написания
|
||
и коды ОКЕИ), упорядоченные перечни кодов форм по редакциям (`66n`, `fsbu4`) и вариантам (`full`, `simplified`) без
|
||
наименований — байтовая копия `docs/contracts/statement-code-orders.yaml` как `rules/statement_code_orders.yaml`,
|
||
диапазоны кодов — и загрузчик словарей. Словаря
|
||
«код → наименование» в пакете MUST NOT быть (Р-3). Пакет SHALL владеть своими типами входа и выхода; конвертацию
|
||
выхода в модель контракта делает хост. Golden-тесты пакета — внутри пакета на синтетических и обезличенных данных;
|
||
тесты на реальных фикстурах остаются в сервисах и соседние репозитории не читают.
|
||
|
||
В tabular-parser, scan-gateway и arm-core пакет SHALL лежать копией вместе с тестами и конфигом линтеров и
|
||
импортироваться по имени `arm_statements`; раскладку, проверку свежести и маркер версии задаёт требование
|
||
«`sync-statements` — копии пакета в потребителях».
|
||
|
||
#### Scenario: Наименований в пакете нет
|
||
- **WHEN** ревьюер ищет в пакете соответствие «код → наименование»
|
||
- **THEN** его нет: в перечне кодов только коды, наименования строк — словарь ядра
|
||
|
||
### Requirement: `sync-statements` — копии пакета в потребителях
|
||
Копия пакета в хосте (`arm-core`, `tabular-parser`, `scan-gateway`) SHALL лежать в `api/arm_statements/` (рядом с
|
||
`api/src/`, импорт `import arm_statements`) вместе с тестами пакета (`api/arm_statements/tests/`); конфиг линтеров пакета в копию не кладётся (применяется конфиг
|
||
хоста); `ruff` и `pytest` хоста SHALL охватывать копию (тесты пакета — в `testpaths`), `mypy` хоста — запускаться и по
|
||
ней (`mypy src arm_statements`, у `arm-core` — `mypy src scripts arm_statements`). Маркер версии — файл
|
||
`api/arm_statements/VERSION` с полным хэшем коммита репозитория `arm-statements`, из которого разложена копия; его пишет
|
||
только скрипт, сервис читает при старте и пишет в результат как `service.statements_version`. Прямые правки копии MUST
|
||
NOT делаться.
|
||
|
||
Копию SHALL раскладывать и проверять скрипт `docs/scripts/sync-statements.sh` (по образцу `sync-web.sh`; цель `make -C
|
||
docs sync-statements`). Источник — локальный checkout `arm-statements` рядом с workspace или `--source <dir>`, ссылка —
|
||
`origin/main` или `--ref`; хост — `--host <имя>` или `--host-dir <dir>`. Без `--check` скрипт раскладывает копию коммита
|
||
`--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). С `--in-place` скрипт SHALL раскладывать
|
||
копию коммита `--ref` в рабочее дерево указанного хоста (существующий worktree) без issue, коммита и PR — для
|
||
подключения хоста и для PR фичи, которой нужна новая версия пакета; при прямой правке копии или недостающей зависимости —
|
||
отказ с кодом `20`, ничего не пишется. Копия
|
||
сравнивается с деревом `arm_statements/` источника без файла `VERSION` (его в источнике нет). `--check` SHALL
|
||
возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов
|
||
в пакете равна `docs/contracts/statement-code-orders.yaml`; `10` — копия равна дереву коммита из `VERSION`, но в
|
||
`--ref` есть более новый (копия отстала), или копия перечня кодов отличается от файла docs (перечень в docs правился
|
||
раньше `arm-statements` или пересинхронизации хоста — предупреждение называет файл); `20` — копия отличается от дерева
|
||
коммита из `VERSION` (прямая правка), `VERSION` нет в истории источника, зависимости пакета (список `dependencies`
|
||
его `pyproject`) отсутствуют в зависимостях хоста или (только у `arm-core`) код словаря имён
|
||
`api/rules/statement_lines.yaml` отсутствует в перечне своей редакции **копии пакета**; `2` — нет checkout источника
|
||
или хоста, неизвестный аргумент. Файлы и коды расхождения SHALL печататься. Порядок правки перечня кодов: PR в docs →
|
||
PR в `arm-statements` (байтовая копия yaml) → `sync-statements` в хостах «сначала ядро»; до конца этой цепочки хосты
|
||
получают `10`, а не `20`.
|
||
|
||
Код пакета SHALL проходить самый строгий конфиг `ruff` и `mypy` среди хостов (пакет проверяется им в своём
|
||
репозитории): конфиг хоста применяется к копии, а правка копии под хост запрещена. Тесты пакета SHALL не зависеть от
|
||
фикстур и `conftest` хоста: в `api/arm_statements/tests/` свой `conftest.py`, а `conftest` хоста MUST NOT содержать
|
||
`autouse`-фикстур без ограничения каталогом `tests/` хоста. Зависимости пакета — только те, что есть в зеркале банка
|
||
(сейчас PyYAML); хост держит их в своих зависимостях с версией зеркала.
|
||
|
||
Цель `statements-check` хоста (`sync-statements.sh --check --host-dir .`, docs — `../docs` или `ARM_KA_DOCS`) SHALL
|
||
входить в `make check-full` (коды `0` и `10` — зелёная, `10` с предупреждением «копия отстала», вписывается в описание PR;
|
||
`20` и `2` — красная) и MUST NOT входить в `make check` и пайплайн платформы.
|
||
|
||
#### Scenario: Прямая правка копии
|
||
- **WHEN** в `tabular-parser` изменён файл копии пакета, а источник в `arm-statements` — нет
|
||
- **THEN** `sync-statements --check` завершается с кодом 20 и называет файл; `make check-full` красный; `make check`
|
||
сервиса этого не проверяет
|
||
|
||
#### Scenario: Копия отстала
|
||
- **WHEN** копия в `scan-gateway` равна коммиту `abc1234`, а в `arm-statements` `origin/main` уже `def5678`
|
||
- **THEN** `--check` завершается с кодом 10, `make check-full` зелёный с предупреждением «копия отстала»
|
||
|
||
#### Scenario: Нет checkout источника
|
||
- **WHEN** рядом нет `arm-statements` и `--source` не задан
|
||
- **THEN** `--check` завершается с кодом 2 и подсказкой, `make check-full` красный
|
||
|
||
#### Scenario: Перечень кодов в docs новее копии
|
||
- **WHEN** в docs в `statement-code-orders.yaml` добавлен код, а `arm-statements` и копии в хостах ещё не обновлены
|
||
- **THEN** `--check` в каждом хосте завершается с кодом 10 и называет файл перечня; `make check-full` зелёный с
|
||
предупреждением, посторонние PR в сервисы не краснеют
|
||
|
||
#### Scenario: Код словаря ядра вне перечня
|
||
- **WHEN** в `arm-core/api/rules/statement_lines.yaml` редакции `fsbu4` есть код 1120, а в перечне `fsbu4` его нет
|
||
- **THEN** `--check` в `arm-core` завершается с кодом 20 и называет код и редакцию
|
||
|
||
#### Scenario: Копия в PR фичи
|
||
- **WHEN** PR scan-gateway нужна версия пакета новее, чем в `main` хоста
|
||
- **THEN** `sync-statements.sh --in-place --host-dir .` в worktree фичи раскладывает копию и пишет `VERSION`, копия
|
||
коммитится отдельным коммитом этого PR; `--check` после раскладки — `0`
|
||
|
||
#### Scenario: Версия общего кода в результате
|
||
- **WHEN** сервис собирает результат копией пакета, у которой в `VERSION` коммит `abc1234` репозитория `arm-statements`
|
||
- **THEN** в результате `service.statements_version = "abc1234"`
|
||
|
||
### Requirement: Шаг 1 — кандидат в форму
|
||
Каждый адаптер шага 1 SHALL выдавать на файл список `FormCandidate`, и только с ним работают шаги 2–8. Поля
|
||
кандидата: `source` (`ocr` | `excel`); `locator` (`file:<имя json>;table:<i>` у OCR, `sheet:<A>[,<B>…]` у Excel);
|
||
`type_hint` (`BALANCE` | `INCOME` | `DISCLOSURE` | `null`); `evidence` (ОКУД и заголовок формы как `Cell`); `header`
|
||
(поля `organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `okei`, `address` — каждое
|
||
`Cell` или `null`); `rows` — плотная сетка строк × ячеек, все страницы и листы формы уже в одной сетке, пустая позиция —
|
||
`null`; `segment_breaks` — номера строк сетки, с которых начинается новый лист Excel (у OCR пусто); `warnings`.
|
||
|
||
`Cell` SHALL быть одной для шапки и таблицы: `text` — как показывает источник; `value` — число или дата, если
|
||
источник их знает (у OCR всегда `null`; у Excel — значение по числовому формату ячейки, tp#42); `confidence` 0–100
|
||
(у Excel всегда 100); `ref` — позиция в источнике (OCR — `row:<row_index ИС>`, Excel — `<лист>!<A1>` по правилу Excel:
|
||
имя листа в одинарных кавычках, если в нём есть что-то кроме букв, цифр и `_` или оно начинается с цифры, апостроф внутри
|
||
имени удваивается).
|
||
|
||
`type_hint` SHALL вычисляться одним правилом у обоих адаптеров: ОКУД по словарю (0710001 → `BALANCE`, 0710002 →
|
||
`INCOME`, прочие 0710xxx → `DISCLOSURE`) → иначе заголовок по словарю заголовков (у OCR заголовок — `label` таблицы
|
||
ИС; «Пояснения», «Расшифровка» → `DISCLOSURE`) → иначе `null`.
|
||
|
||
#### Scenario: Excel-ячейка с числовым форматом
|
||
- **WHEN** в Excel ячейка хранит 106311116.46 с форматом `#,##0,`
|
||
- **THEN** у `Cell` `text = "106 311"`, `value = 106311`, `confidence = 100`, `ref = "Баланс!D27"` (лист и адрес
|
||
ячейки)
|
||
|
||
#### Scenario: Имя листа с пробелом
|
||
- **WHEN** код строки стоит в ячейке C14 листа «Бухгалтерский баланс. Лист 1»
|
||
- **THEN** `ref = "'Бухгалтерский баланс. Лист 1'!C14"`
|
||
|
||
#### Scenario: OCR-ячейка
|
||
- **WHEN** в таблице ИС ячейка строки `row_index = 8` содержит `"[190 :"` с уверенностью 25
|
||
- **THEN** у `Cell` `text = "[190 :"`, `value = null`, `confidence = 25`, `ref = "row:8"`
|
||
|
||
#### Scenario: Тип по ОКУД против заголовка
|
||
- **WHEN** у блока формы ОКУД 0710002, а заголовок «Бухгалтерский баланс»
|
||
- **THEN** `type_hint = INCOME`; окончательный тип выбирается по кодам на шаге 6
|
||
|
||
### Requirement: Шаг 1 — шапка кандидата
|
||
У каждого кандидата SHALL быть своя шапка. Текст каждого поля разбирается общим кодом по `Cell.text`: ИНН — только
|
||
цифры, без замены двойников, длина 10 или 12, иначе `null`; дата отчёта — словарём периодов (тот же, что для подписей
|
||
колонок, шаг 2); единицы — словарём единиц `statement_units.yaml` по тексту, а если по тексту не найдены — по коду ОКЕИ
|
||
тем же словарём (384 → «тыс. руб.», 383 → «руб.», 385 → «млн руб.»); ОКЕИ — только цифры. Сырой текст и уверенность
|
||
каждого поля SHALL доезжать до контракта (`<поле>_text`, `<поле>_confidence`).
|
||
|
||
#### Scenario: ИНН с мусором клеток
|
||
- **WHEN** поле ИНН шапки OCR — `"3|1 21 2|"` с цифрами ИНН между вертикальными чертами
|
||
- **THEN** ИНН собирается только из цифр, `|` единицей не считается; `inn_text` хранит исходный текст
|
||
|
||
#### Scenario: Единицы только кодом ОКЕИ
|
||
- **WHEN** в шапке Excel-формы нет текста единиц, а в блоке «Коды» стоит ОКЕИ 384
|
||
- **THEN** `units = "тыс. руб."`, `okei = "384"`, предупреждения `UNITS_NOT_FOUND` нет
|
||
|
||
### Requirement: Очистка OCR-текста — первая стадия типовых разборов
|
||
Очистка OCR SHALL выполняться не отдельным шагом, а первой стадией разборов даты, подписи колонки, кода строки,
|
||
разлепления склеенной строки и суммы — для любой ячейки без `value` (у Excel — только текстовые ячейки). Буквы-
|
||
двойники SHALL заменяться в датах, подписях, кодах и суммах (Р-11): `О/о/O/o → 0`, `З/з → 3`, `б → 6`, `S/s → 5`,
|
||
`I/l/|/і → 1`, `Ч → 4` — только вплотную к цифре, в несколько проходов; слова не портятся. В суммах крайние `|`
|
||
(след границы таблицы) SHALL убираться, а не заменяться. ИНН двойники MUST NOT трогать. Сумма с заменой двойника
|
||
получает `is_recognized = false`; код с заменой считается неуверенным. Текст длиннее 200 символов разбором даты не
|
||
обрабатывается (`null`).
|
||
|
||
#### Scenario: Двойник в сумме
|
||
- **WHEN** ячейка суммы OCR — `"1 2О5"` с уверенностью 100
|
||
- **THEN** `amount = 1205`, `is_recognized = false`, `text = "1 2О5"`
|
||
|
||
#### Scenario: Двойник в коде
|
||
- **WHEN** ячейка кода — `"I1О0"` с уверенностью 100
|
||
- **THEN** код разбирается как `1100` и считается неуверенным (подтверждается структурой, шаг 8)
|
||
|
||
#### Scenario: Граница таблицы в сумме
|
||
- **WHEN** ячейка суммы — `"| : 122477"`
|
||
- **THEN** крайняя `|` удаляется, а не становится единицей; сумма 122477, `is_recognized = false`
|
||
|
||
### Requirement: Шаг 1 — многостраничная форма
|
||
Требование относится к адаптеру Excel: у OCR каждая таблица ИС — отдельный кандидат, склейки таблиц нет (повторная
|
||
шапка внутри таблицы ИС — сегмент шага 2). У Excel одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
|
||
«Форма … с.2» или по позиции листа; граница листа записывается в `segment_breaks`. Тип из маркера продолжения SHALL
|
||
сверяться с `type_hint` формы: расхождение — отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT`
|
||
(`error`). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и
|
||
более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат и предупреждение
|
||
`SHEET_NOT_JOINED` (`error`).
|
||
|
||
#### Scenario: Лист-продолжение с одной строкой
|
||
- **WHEN** после листа ОФР идёт лист без шапки с единственной строкой 2400
|
||
- **THEN** лист присоединяется к ОФР (код в диапазоне формы, кодов меньше трёх — число колонок не сверяется), граница
|
||
листа — в `segment_breaks`
|
||
|
||
#### Scenario: Лист другой ширины с тремя кодами
|
||
- **WHEN** лист без маркера и шапки содержит три кода ББ и четыре колонки сумм при трёх у открытой формы
|
||
- **THEN** лист — отдельный кандидат, предупреждение `SHEET_NOT_JOINED` уровня `error`
|
||
|
||
### Requirement: Диапазоны кодов и разбор кода
|
||
Словарь диапазонов кодов SHALL содержать отрезки: ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала
|
||
3100–3699, отчёт о движении денежных средств 4100–4499, пояснения 5000–5999, отчёт о целевом использовании
|
||
6100–6499. Кодовые диапазоны для поиска столбца — объединение отрезков. Разбор кода: убрать пробелы и переносы →
|
||
заменить двойники (`|` → 1 только если результат попадает в кодовый диапазон) → убрать краевую пунктуацию → 4–5 цифр
|
||
(допускается хвост `.0`), первые четыре — в кодовом диапазоне. Многострочная ячейка — код, только если каждая её часть
|
||
код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца.
|
||
|
||
#### Scenario: Год и сумма — не код
|
||
- **WHEN** в ячейке «2024» или четырёхзначная сумма «1050» или «7450»
|
||
- **THEN** кодом это не считается (вне отрезков); «3450» — код (отрезок 3100–3699 отчёта об изменениях капитала)
|
||
|
||
#### Scenario: Краевая пунктуация и черта
|
||
- **WHEN** ячейка кода — `"1220 ,"` или `"| 140"`
|
||
- **THEN** коды `1220` и `1140`
|
||
|
||
### Requirement: Шаг 2 — столбец кодов
|
||
Столбец кодов SHALL находиться одним правилом: (а) столбец с подписью из словаря подписей кода («код», «коды», «код
|
||
строки», «код показателя» — после нормализации регистра и пунктуации) над первой строкой с кодом, если под подписью
|
||
доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3; при нескольких кандидатах — с наибольшим числом кодов; берётся
|
||
последняя такая подпись над первой строкой с кодом; (б) иначе — столбец с наибольшим числом ячеек-кодов при кодах ≥ 3
|
||
и доле ≥ 0,6; иначе предупреждение `CODE_COLUMN_NOT_FOUND`.
|
||
|
||
#### Scenario: Титульный блок 1С с подписью «Коды»
|
||
- **WHEN** под подписью «Коды» титульного блока выгрузки 1С стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ 12267, ОКФС, ОКЕИ
|
||
(один код из девяти, доля 0,11), а коды строк формы — в другом столбце
|
||
- **THEN** столбец под «Коды» не выбирается; столбец кодов — по правилу (б)
|
||
|
||
#### Scenario: Документ из двух строк
|
||
- **WHEN** форма содержит две строки с кодами под подписью «Код»
|
||
- **THEN** столбец кодов найден (два из двух, доля 1,0)
|
||
|
||
### Requirement: Шаг 2 — сегменты, повторная шапка и границы листов
|
||
Раскладка SHALL состоять из сегментов. Новый сегмент открывает строка-шапка (в ней есть ячейка из словаря подписей
|
||
кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница
|
||
листа из `segment_breaks`. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если
|
||
сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона
|
||
столбца первого сегмента, минимум один. Колонки документа (`columns[]`) SHALL задаваться первым сегментом; колонки
|
||
любого следующего сегмента новых `columns[]` не создают, а сопоставляются с колонками первого сегмента:
|
||
- сегмент со своей шапкой (повторная шапка OCR, лист-продолжение Excel со своей строкой заголовка) — по `period_date`,
|
||
если у каждой колонки сегмента дата есть и совпадает с датой ровно одной колонки первого сегмента; иначе по порядку;
|
||
- сегмент без шапки — колонки значений — столбцы правее кода, где в строках с кодами есть числа, сопоставление по
|
||
порядку.
|
||
|
||
Значения сегмента несут `column_index` сопоставленной колонки первого сегмента. При сопоставлении по порядку число
|
||
колонок сегмента не совпало с первым — предупреждение `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции в
|
||
пределах меньшего числа колонок,
|
||
лишние столбцы сегмента в `columns[]` не попадают, их номера в сетке перечислены в `message`. Столбец кодов не найден ни
|
||
так, ни так — предупреждение `SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`,
|
||
статусы по шагу 3).
|
||
|
||
#### Scenario: Лист-продолжение НПЗ со своей шапкой
|
||
- **WHEN** у ББ НПЗ («Бухгалтерский баланс. Лист 4») своя строка заголовка, колонки периодов — в столбцах 8–10, у первого
|
||
листа — в столбцах 16, 21, 24, даты подписей те же (30.09.2025, 31.12.2024, 31.12.2023)
|
||
- **THEN** новых колонок нет: значения листа 4 получают `column_index` 16, 21, 24 по совпадению `period_date`;
|
||
предупреждения нет, §7 ББ считается по одной колонке на дату, документ остаётся `done`
|
||
|
||
#### Scenario: Повторная шапка с другими подписями
|
||
- **WHEN** повторная шапка второй страницы скана содержит две колонки без разобранной даты, у первого сегмента — две
|
||
колонки с датами
|
||
- **THEN** сопоставление по порядку, значения несут номера колонок первого сегмента, предупреждения нет
|
||
|
||
#### Scenario: Сегмент шире первого
|
||
- **WHEN** у сегмента со своей шапкой четыре колонки периодов, у первого — три
|
||
- **THEN** `SEGMENT_COLUMNS_MISMATCH` уровня `error`, значения первых трёх — по позиции, номер четвёртого столбца — в
|
||
`message`
|
||
|
||
#### Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке
|
||
- **WHEN** второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом
|
||
листе
|
||
- **THEN** у сегмента свой столбец кодов (ослабленный поиск), значения строки 2400 не сдвинуты
|
||
|
||
#### Scenario: Повторная шапка на второй странице скана
|
||
- **WHEN** в таблице ИС посередине стоит повторная строка «Наименование показателя | Код | На 30.09.2025 | …»
|
||
- **THEN** с неё начинается новый сегмент со своей раскладкой; строка-шапка в данные не попадает
|
||
|
||
### Requirement: Шаг 2 — строка заголовка, нумерация и роли строк
|
||
Строка заголовка SHALL быть последней над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе
|
||
последней над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом);
|
||
двухстрочный заголовок склеивается. Строка нумерации SHALL получать роль `numbering` и в данные не попадать; строка —
|
||
нумерация, если в ней не меньше трёх непустых ячеек, каждая после замены букв-двойников — целое число, и числа слева
|
||
направо возрастают на 1, начинаются с 1 или 2 и допускают не больше одного пропуска. Роль
|
||
`title` — только строки выше найденной строки заголовка; в сегменте без заголовка роли `title` нет. Заголовка нет ни у
|
||
одного сегмента документа → колонки значений первого сегмента — столбцы правее кода, где в строках с кодами числа,
|
||
`label = ""`, `period_date = null`, `period_kind = none`, одно предупреждение `HEADER_ROW_NOT_FOUND` (`error`).
|
||
Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки без предупреждения.
|
||
|
||
#### Scenario: Нумерация в скане (AURORA-1517)
|
||
- **WHEN** под шапкой таблицы ИС строка «1 | 2 | 3 | 4 | 5»
|
||
- **THEN** роль `numbering`, строки данных из неё нет
|
||
|
||
#### Scenario: Нумерация с двойником
|
||
- **WHEN** строка нумерации скана распознана как «1 | 2 | З | 4»
|
||
- **THEN** «З» читается как 3, роль `numbering`; строки `unreadable` с «суммами» 3 и 4 нет
|
||
|
||
#### Scenario: Нумерация без первой ячейки
|
||
- **WHEN** ИС потеряла первую ячейку нумерации: «2 | 3 | 4 | 5»
|
||
- **THEN** роль `numbering` (начало с 2 допускается)
|
||
|
||
#### Scenario: Нумерация с пропуском
|
||
- **WHEN** строка «1 | 2 | 4 | 5» (тройка не распознана)
|
||
- **THEN** роль `numbering` (один пропуск допускается); «1 | 3 | 5» — не нумерация
|
||
|
||
#### Scenario: Нумерация в Excel со «2» в столбце кодов
|
||
- **WHEN** в форме Межениновской под шапкой строка «1 | 2 | 3 | 4 | 5», где «2» стоит в столбце кодов
|
||
- **THEN** роль `numbering`; «2» кодом не считается
|
||
|
||
#### Scenario: Листы 2–5 ББ без шапки
|
||
- **WHEN** у первого листа ББ заголовок есть, у листов 2–5 — нет
|
||
- **THEN** листы 2–5 берут колонки первого, предупреждения о заголовке нет, документ может быть `done`
|
||
|
||
### Requirement: Шаг 2 — подпись и дата колонки
|
||
Подпись колонки SHALL разбираться одним словарём периодов `statement_periods.yaml`, объединяющим словарь
|
||
tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dates.py::test_parses_real_forms` scan-gateway —
|
||
обязательные тесты словаря. Результат — `period_date` (конец периода колонки) и `period_kind`:
|
||
- абсолютная подпись («На 31 марта 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «с
|
||
01.01.2025 по 31.03.2025», «2024», хвост «0:00:00», клетки «2|0|2|4|») → `absolute`;
|
||
- относительная: «На отчётную дату», «За отчётный период» → `relative:current` (= эффективная дата); «На 31 декабря
|
||
предыдущего года» → `relative:prev_year_end` (31.12 года эффективной даты − 1); «На 31 декабря года,
|
||
предшествующего предыдущему» → `relative:prev_prev_year_end` (31.12 года − 2); «За аналогичный период предыдущего
|
||
года» → `relative:prev_period` (эффективная дата − 1 год, 29 февраля → 28 февраля);
|
||
- подписи нет → `none`.
|
||
|
||
Эффективная дата документа SHALL быть датой отчёта из шапки, иначе самой поздней абсолютной датой колонок; нет ни
|
||
той, ни другой → у относительных колонок `period_date = null`, `period_kind` остаётся относительным.
|
||
|
||
Подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») SHALL пропускаться с
|
||
предупреждением `COLUMN_SKIPPED` (`info`). Подпись, не разобранная ни как период, ни как исключение, при числах в
|
||
столбце в строках с кодами SHALL оставлять колонку с подписью как есть, `period_date = null`, `period_kind = none` и
|
||
предупреждением `COLUMN_WITHOUT_DATE` (`error`, `column_index`); без чисел — пропуск. Подпись OCR с уверенностью ниже
|
||
100, разобранная в дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два
|
||
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`,
|
||
`column_index`). Уверенность подписи — `label_confidence` (у Excel 100; подписи нет — `null`).
|
||
|
||
Если среди колонок значений сегмента есть хотя бы одна с подписью периода (абсолютной или относительной), столбец без
|
||
текста подписи правее **последней** такой колонки SHALL НЕ считаться колонкой сумм: его числа не попадают в значения
|
||
строк, `COLUMN_WITHOUT_DATE` не выдаётся, вместо этого выдаётся одно предупреждение `NUMBERS_OUTSIDE_TABLE` (`info`) с
|
||
адресами ячеек, собранными по всем сегментам формы (в тексте — первые 10, дальше «и ещё N»). Предупреждение SHALL НЕ
|
||
влиять на статус документа. Решение владельца 18.10.2026 (Р-44). Правило общее для Excel и OCR и живёт в пакете.
|
||
У сегмента без подписанных колонок (лист-продолжение без шапки) столбцы без текста подписи сверх числа колонок документа
|
||
SHALL считаться числами правее таблицы. Подписанный столбец без чисел в строках с кодами границей таблицы не служит.
|
||
Адрес ячейки — `<лист>!<A1>` у Excel и `row:<n>` у OCR. Без подписанных колонок, а также для столбца без подписи между
|
||
подписанными и для столбца с любым текстом подписи, не разобранным как период или исключение, поведение прежнее (`COLUMN_WITHOUT_DATE`).
|
||
|
||
Подтверждение SHALL опираться только на уверенные источники, чтобы неуверенное не подтверждало само себя: для
|
||
неуверенной подписи эффективная дата — дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с
|
||
уверенной подписью (`label_confidence = 100`); такой нет — подпись не подтверждена. Неуверенная дата отчёта (шаг 8, поля
|
||
шапки) подтверждается только самой поздней абсолютной датой колонок с уверенной подписью. Для периода документа и
|
||
относительных подписей эффективная дата считается, как выше, по всем источникам.
|
||
|
||
#### Scenario: Бланк 66н без впечатанных дат
|
||
- **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года,
|
||
предшествующего предыдущему», дата отчёта шапки 30.09.2025
|
||
- **THEN** `period_date` колонок 2025-09-30, 2024-12-31, 2023-12-31, `period_kind` — `relative:current`,
|
||
`relative:prev_year_end`, `relative:prev_prev_year_end`; предупреждений о дате колонок нет
|
||
|
||
#### Scenario: Неуверенная подпись не подтверждает сама себя
|
||
- **WHEN** даты отчёта в шапке нет, единственная колонка с абсолютной датой — подпись OCR «На 30.09.2О25» с уверенностью
|
||
60, остальные колонки без подписи
|
||
- **THEN** эффективная дата для подтверждения строится без этой подписи — уверенных источников нет, подпись не
|
||
подтверждена: `COLUMN_DATE_UNCERTAIN` уровня `error`; период документа (`period_known`) по-прежнему определяется этой
|
||
датой
|
||
|
||
#### Scenario: Мусорная подпись над числами
|
||
- **WHEN** подпись колонки — `"| | г"` с уверенностью 17, а в колонке суммы
|
||
- **THEN** колонка остаётся, `period_date = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня
|
||
`error` с `column_index`, документ `needs_check`
|
||
|
||
#### Scenario: Числа справа от таблицы
|
||
- **WHEN** у ББ подписи периодов стоят над столбцами M, Q, S, а правее таблицы в строках с кодами лежат формулы
|
||
аналитика (V45, W57, U58, V58, V72, W72) над пустой шапкой (Межениновская 2025-Q3)
|
||
- **THEN** колонок значений три, числа U–W в значения не попадают, `COLUMN_WITHOUT_DATE` и `SEGMENT_COLUMNS_MISMATCH`
|
||
нет; одно предупреждение `NUMBERS_OUTSIDE_TABLE` уровня `info` с адресами этих ячеек; документ `done`, если нет других
|
||
причин
|
||
|
||
#### Scenario: Столбец без подписи между подписанными
|
||
- **WHEN** между колонками с подписями периода стоит столбец без подписи с числами
|
||
- **THEN** он остаётся колонкой с `COLUMN_WITHOUT_DATE` уровня `error`, `NUMBERS_OUTSIDE_TABLE` нет
|
||
|
||
#### Scenario: Подписанных колонок нет
|
||
- **WHEN** ни у одного столбца значений нет подписи периода
|
||
- **THEN** правило «числа справа от таблицы» не применяется, столбцы с числами — колонки с `COLUMN_WITHOUT_DATE`
|
||
|
||
#### Scenario: Колонка «Изменение»
|
||
- **WHEN** правее периодов колонка «Изменение, %»
|
||
- **THEN** колонка пропускается, предупреждение `COLUMN_SKIPPED` уровня `info`
|
||
|
||
### Requirement: Шаг 2 — номер колонки
|
||
`column_index` колонки значений (`columns[]`, `values[]`, предупреждения) SHALL быть номером столбца в сетке кандидата
|
||
шага 1: у OCR — `column_index` ИС как есть (не обязательно подряд), у Excel — номер столбца листа первого сегмента с 0.
|
||
Номер уникален в документе; колонки сегментов без шапки сопоставляются с колонками первого сегмента по порядку и несут их
|
||
номера. Предупреждение о столбце, которого нет среди `columns[]` (`COLUMN_SKIPPED`), SHALL нести номер этого столбца в
|
||
сетке.
|
||
|
||
#### Scenario: Разреженные колонки ИС
|
||
- **WHEN** в таблице ИС код стоит в колонке 1, суммы — в колонках 2, 4, 5 (колонки 3 нет)
|
||
- **THEN** `columns[].column_index` — 2, 4, 5, у значений — те же номера
|
||
|
||
#### Scenario: Пропущенная колонка Excel
|
||
- **WHEN** на листе Excel коды в столбце C, периоды в D и E, «Изменение, %» в F
|
||
- **THEN** `columns[].column_index` — 3 и 4, предупреждение `COLUMN_SKIPPED` с `column_index = 5`
|
||
|
||
### Requirement: Шаг 2 — столбец наименования
|
||
Столбец наименования SHALL быть столбцом с подписью, начинающейся с «наименование» (после нормализации), в строке
|
||
заголовка; иначе — столбцом левее кода с наибольшим числом текстов, кроме столбца «Пояснения». Наименование строки —
|
||
ячейка этого столбца; пустая — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
|
||
|
||
#### Scenario: Наименование без подписи
|
||
- **WHEN** у формы нет подписи «Наименование показателя», а левее кода два столбца — «Пояснения» (короткие номера) и
|
||
тексты статей
|
||
- **THEN** наименование берётся из столбца текстов статей
|
||
|
||
### Requirement: Шаг 3 — строки данных и их статусы
|
||
Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: в ячейке кода есть
|
||
хотя бы одна цифра (разобранный код или нет), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`), или
|
||
непуста с уверенностью ниже 100, или её текст — «цифра под видом буквы»: после замены двойников без условия «вплотную к
|
||
цифре» в нём есть цифра и нет других букв (`'I'`, `'| О'`). Строка только с текстом наименования — не строка данных;
|
||
текст из настоящих букв в колонке значений при уверенности 100 (подписи «Руководитель … Косарев», «(расшифровка
|
||
подписи)» под формой Excel) содержимым не считается — осознанное исключение из «ничего не выбрасывать молча». У строки — `row_index` (порядковый номер в документе с 0,
|
||
по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>` по правилу шага 1, без столбца кодов — первой непустой
|
||
ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`.
|
||
Статусы:
|
||
- `ok` — код разобран;
|
||
- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр (в том числе только из
|
||
букв-двойников вне соседства с цифрой: «изо :», «}», «:») считается пустой;
|
||
- `detail` — строка без кода с содержимым, над которой (в том же или в предыдущих сегментах документа) есть строка `ok`;
|
||
`parent_ref` — `ref` ближайшей строки `ok` выше (строки `unreadable` / `missing` между ними пропускаются);
|
||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки `ok`; строки роли `title`
|
||
сюда не попадают.
|
||
|
||
Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не
|
||
статус и не предупреждение: все строки с кодом — `ok`, порядок как в файле.
|
||
|
||
#### Scenario: Код-мусор (AURORA-1462)
|
||
- **WHEN** ячейка кода — `"ИЧ,250"`, в колонках суммы
|
||
- **THEN** строка `unreadable` с `line_code_text = "ИЧ,250"`, суммами и `ref`; строка не пропадает
|
||
|
||
#### Scenario: Мусор в ячейке кода без значений
|
||
- **WHEN** в ББ aep_6 ячейка кода — `"44 30\nI |"`, ячейки значений пусты
|
||
- **THEN** строка `unreadable` без значений: документ `needs_check` (не все строки `ok` / `detail`), подтверждение она не
|
||
блокирует (`LINES_UNRESOLVED` — только строки со значениями)
|
||
|
||
#### Scenario: Расшифровка «в том числе»
|
||
- **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
|
||
- **THEN** строка `detail`, `parent_ref` = `ref` строки 1150
|
||
|
||
#### Scenario: Расшифровка под строкой-мусором
|
||
- **WHEN** порядок строк: 1150 (`ok`), «ИЧ,250» (`unreadable`), строка без кода с суммой
|
||
- **THEN** последняя — `detail` с `parent_ref` строки 1150 (ближайшая строка `ok`, `unreadable` пропускается)
|
||
|
||
#### Scenario: Код из букв-двойников без цифр (aep_3)
|
||
- **WHEN** ячейка кода — `"изо :"`, в колонке значений `'3'` с уверенностью 0, выше есть строки `ok`
|
||
- **THEN** цифр в ячейке кода нет — она пустая, строка `detail`; если это потерянная строка 1130, её сумма не войдёт в
|
||
1100, и соотношение `1100 = …` подсветит раздел (цена правила, S-61)
|
||
|
||
#### Scenario: Цифра под видом буквы (aep_6)
|
||
- **WHEN** в строке без кода под строкой `ok` единственная непустая ячейка значений — `'I'` с уверенностью 100
|
||
- **THEN** строка данных `detail` (раньше терялась молча); значение `amount = null`, `string_value = "I"`,
|
||
`is_recognized = false`
|
||
|
||
#### Scenario: Потерянный код в начале таблицы
|
||
- **WHEN** первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет
|
||
- **THEN** строка `missing`
|
||
|
||
#### Scenario: Повтор кода (AURORA-1406)
|
||
- **WHEN** код 2410 стоит у двух строк с разными наименованиями
|
||
- **THEN** обе строки `ok`, порядок как в файле, предупреждения нет
|
||
|
||
### Requirement: Шаг 3 — разлепление склеенных строк
|
||
До разбора кода SHALL выполняться разлепление: ячейка кода содержит N ≥ 2 частей через перевод строки, и каждая
|
||
непустая ячейка значений — ровно N частей; разлепление делается, если хотя бы одна часть кода разобралась как код
|
||
диапазона столбца. Получается N строк с `ref` `row:<i>#<k>`; нечитаемая часть кода — строка `unreadable`; наименование —
|
||
k-я из непустых частей, если их тоже N, иначе целиком у каждой; `line_code_text` и `text` значений — k-е части. Значения разлепленных строк получают
|
||
`is_recognized = false`; разлепление — предупреждение `ROW_SPLIT` (`info`). Число частей не совпало или ни одна часть
|
||
не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`.
|
||
|
||
#### Scenario: Склейка «1500\n1700» (OQ-156)
|
||
- **WHEN** ячейка кода — `"1500\n1700"`, суммы — `"917 775\n2 371 333"`
|
||
- **THEN** две строки `ok` (`row:i#1`, `row:i#2`) с суммами 917775 и 2371333, `is_recognized = false`, предупреждение
|
||
`ROW_SPLIT`; при сошедшихся соотношениях §7 значения не подсвечиваются (Р-17)
|
||
|
||
#### Scenario: Часть не из диапазона
|
||
- **WHEN** в ББ ячейка кода — `"44 30\nI |"`
|
||
- **THEN** строка не разлепляется (4430 вне диапазона ББ)
|
||
|
||
### Requirement: Шаг 4 — значение ячейки
|
||
Шаг 4 SHALL выдавать `StatementValue {column_index, amount, string_value, text, confidence, is_recognized}`: `text` —
|
||
всегда сырой текст источника; `string_value` — только когда `amount = null` и текст непуст и не прочерк. Правила:
|
||
- `value` — число: `amount` квантуется до 2 знаков half-up; больше 16 целых цифр → `amount = null`, `string_value` =
|
||
текст, `is_recognized = false`, предупреждение `VALUE_OUT_OF_RANGE` (`info`);
|
||
- `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||
- `value` нет → `parse_amount(text, source)` по стадиям:
|
||
1. пусто или прочерк (`-`, `–`, `—`, `−` без цифр) → `amount = null`, `is_recognized = true`; многострочное значение
|
||
в строке с однострочным кодом → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||
2. крайние `|` (след границы таблицы) удаляются до замены двойников, а не становятся единицей (`"|122477"` → 122477);
|
||
затем буквы-двойники вплотную к цифре → цифры (Р-11); и то и другое — исправление (`is_recognized = false`);
|
||
3. **ядро числа** — участок от первого из символов «цифра, `(`, `-`, `−`, `+`» до последнего из «цифра, `)`, `}`,
|
||
`]`»; всё вне ядра — **краевой мусор** (`|`, `:`, `=`, `■`, `•`, кавычки, точки, `{`, `[` и любые другие символы):
|
||
выбрасывается, а если среди выброшенного есть что-то кроме пробелов — `is_recognized = false`;
|
||
4. в ядре допустимы только цифры, пробелы (в том числе NBSP и тонкие), `,`, `.`, знак (`-`, `−` как минус, `+`) в
|
||
начале и одна пара скобок вокруг всего числа: `(` первым символом и `)`, `}` или `]` последним (`}` / `]` —
|
||
исправление); `{` и `[` открывающей скобкой не считаются. Иначе — буква в ядре (после стадии 2), скобка без пары
|
||
или не на краю ядра, знак не в начале, любой другой символ между цифрами (`:`, `/`, `=`, `|`) — `amount = null`,
|
||
`string_value` = текст, `is_recognized = false`;
|
||
5. цифр нет вовсе → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||
6. скобки → минус; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка —
|
||
тысячи при `[+-]?[1-9]\d*(\.\d{3})+`, иначе десятичный разделитель, три и более знаков после него → `amount = null`,
|
||
`string_value` = текст; у `excel` запятая и точка — десятичный разделитель; пробел допустим только как разделитель
|
||
тысяч (группы ровно по три цифры), иначе `amount` — цифры подряд («лучшая догадка», не длиннее 16 цифр, иначе
|
||
`null`) и `is_recognized = false`;
|
||
- `is_recognized = true` — текст разобран как число без выброшенных и исправленных символов (замена двойника, `}` или
|
||
`]` вместо скобки — исправление); `confidence` — от источника и в `is_recognized` не входит;
|
||
- позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence`
|
||
ячейки (позиции нет — 100).
|
||
|
||
`parse_amount` — единственное место шагов 2–8, где разбор зависит от `source`. Разбор сумм ОСВ этим правилом MUST NOT
|
||
затрагиваться.
|
||
|
||
#### Scenario: Потерянная цифра (AURORA-1511)
|
||
- **WHEN** ячейка OCR — `"42831 1"` с уверенностью 100
|
||
- **THEN** `amount = 428311`, `is_recognized = false`, `text = "42831 1"`
|
||
|
||
#### Scenario: Скобка `}` (OQ-147)
|
||
- **WHEN** ячейка OCR — `"(7744536}"`
|
||
- **THEN** `amount = -7744536`, `is_recognized = false`
|
||
|
||
#### Scenario: Скобка внутри цифр (aep_3)
|
||
- **WHEN** ячейка OCR — `"211(1 |"`
|
||
- **THEN** крайняя `|` — краевой мусор; в ядре `211(1` скобка без пары и не на краю → `amount = null`, `string_value =
|
||
"211(1 |"`, `is_recognized = false`
|
||
|
||
#### Scenario: Буква в ядре числа (aep_6)
|
||
- **WHEN** ячейка OCR — `"(Ю4)"`
|
||
- **THEN** «Ю» — не двойник и стоит в ядре → `amount = null`, `string_value = "(Ю4)"`, `is_recognized = false` (не −4)
|
||
|
||
#### Scenario: Краевой мусор с двух сторон
|
||
- **WHEN** ячейка OCR — `": : 2856983 = ■"` или `"| : 122477"`
|
||
- **THEN** `amount = 2856983` и `amount = 122477`, `is_recognized = false` (выброшен мусор вне ядра)
|
||
|
||
#### Scenario: Фигурная скобка не открывает
|
||
- **WHEN** ячейка OCR — `"{38378)"`
|
||
- **THEN** `{` — краевой мусор, в ядре `38378)` закрывающая скобка без пары → `amount = null`, `string_value = "{38378)"`,
|
||
`is_recognized = false`
|
||
|
||
#### Scenario: Запятая по источнику
|
||
- **WHEN** текст `"4,200"`
|
||
- **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20`
|
||
|
||
#### Scenario: Длинное тире (OQ-148, закрыт решением владельца 18.10.2026)
|
||
- **WHEN** ячейка OCR — `"—"` с уверенностью 100
|
||
- **THEN** `amount = null`, `string_value = null`, `is_recognized = true`
|
||
|
||
### Requirement: Шаг 4 — единицы и дробные значения
|
||
При единицах «тыс. руб.» и доле значений с дробной частью ≥ 10 % SHALL выдаваться предупреждение документа
|
||
`FRACTIONAL_IN_THOUSANDS` (`info`); то же сочетание в книге xlsb без числовых форматов — `XLSB_SCALE_SUSPECT`
|
||
(`error`). Книга xlsb без числовых форматов сама по себе — `XLSB_NO_FORMATS` (`info`); формат ячейки не разобран —
|
||
`NUMBER_FORMAT_UNPARSED` (`info`, одно на документ с перечнем форматов).
|
||
|
||
#### Scenario: xlsb с копейками при «тыс. руб.»
|
||
- **WHEN** книга xlsb без стилей, в шапке «тыс. руб.», 40 % значений дробные
|
||
- **THEN** предупреждение `XLSB_SCALE_SUSPECT` уровня `error`, документ `needs_check`
|
||
|
||
### Requirement: Шаг 5 — строка формы
|
||
Шаг 5 SHALL выдавать `StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence,
|
||
line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code` — разобранный код (4–5 цифр) или
|
||
`null` у `unreadable` / `missing` / `detail`; `line_code_text` — сырой текст ячейки кода; `line_code_confidence` — от
|
||
источника, у кода с заменой двойника — не выше 99; `line_name` — распознанный текст наименования как есть, с
|
||
уверенностью источника. Сервисы MUST NOT подставлять наименование из справочника и MUST NOT срезать уверенность кода
|
||
по справочнику (Р-3). Редакция (`66n` / `fsbu4`) в сервисах нужна только для перечня кодов (шаг 8): проверяются обе
|
||
редакции, эффективная дата (`fsbu4` при дате после 2024-12-31) разрешает только равенство.
|
||
|
||
#### Scenario: Наименование из OCR (AURORA-1522)
|
||
- **WHEN** у строки 1230 OCR-наименование «Дебиторская задолжен-ность»
|
||
- **THEN** `line_name = "Дебиторская задолжен-ность"` с уверенностью ячейки; справочное имя показывает ядро
|
||
|
||
### Requirement: Шаг 6 — тип формы, вариант и документ
|
||
Шаг 6 SHALL собирать `StatementDocument {document_type, form_variant, locator, header, columns, lines}`. Тип —
|
||
большинство кодов строк `ok` в отрезке ББ → `BALANCE`, ОФР → `INCOME`, прочих форм и пояснений → `DISCLOSURE`;
|
||
совпало с `type_hint` или `type_hint = null` — тип по кодам; противоречит — тип по кодам и предупреждение
|
||
`TYPE_CONFLICT` (`error`); кодов нет — `type_hint`. Поиск столбца кодов (шаг 2) SHALL выполняться всегда, в том числе
|
||
при `type_hint = DISCLOSURE`: если большинство кодов в отрезке ББ или ОФР, тип — по кодам и `TYPE_CONFLICT`. Без столбца
|
||
кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND`
|
||
уровня `error`); `type_hint` нет → у `ocr` таблица ИС с содержимым — предупреждение **файла** `TABLE_LOST` (`error`,
|
||
`locator` таблицы), таблица ИС без содержимого — предупреждение **файла** `TABLE_SKIPPED` (`info`, `locator` таблицы);
|
||
у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа во всех
|
||
этих случаях нет. Содержимое таблицы ИС — хотя бы одна ячейка с цифрой или с «цифрой под видом буквы» (шаг 3); таблица
|
||
только из текста (подписи, пояснительный текст) содержимого не имеет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7
|
||
не выполняются: `columns` и `lines` — пустые массивы, `checks` нет, документ `done`.
|
||
|
||
`form_variant` SHALL определяться только по структуре кодов всех строк `ok` документа (независимо от уверенности
|
||
кода; у ядра — заново при каждом пересчёте после правки, точка входа ниже): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
|
||
ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД
|
||
признаком варианта не служит. Шапка документа — поля `header` кандидата: `text` → `<поле>_text`, разобранное → поле,
|
||
`confidence` → `<поле>_confidence`.
|
||
|
||
#### Scenario: Упрощённый баланс
|
||
- **WHEN** в ББ есть 1150, 1210, 1600, 1310, 1700 и нет 1100 и 1200
|
||
- **THEN** `form_variant = simplified`; шаг 7 и перечень кодов берут упрощённый набор
|
||
|
||
#### Scenario: «Пояснения» с кодами баланса
|
||
- **WHEN** ИС подписала таблицу «Пояснения к бухгалтерскому балансу» (`type_hint = DISCLOSURE`), а в столбце кодов —
|
||
1110…1700
|
||
- **THEN** столбец кодов найден, `document_type = BALANCE`, предупреждение `TYPE_CONFLICT` уровня `error`; данные не
|
||
выброшены
|
||
|
||
#### Scenario: Лист «прочее»
|
||
- **WHEN** в книге Excel лист «Примечания» без заголовка формы, ОКУД и кодов
|
||
- **THEN** документа нет; в `metrics.warnings` — `SHEET_OTHER` уровня `info` с `locator = "sheet:Примечания"`
|
||
|
||
#### Scenario: Заголовок спорит с кодами
|
||
- **WHEN** `type_hint = BALANCE`, а коды строк — 2110, 2120, 2100, 2200
|
||
- **THEN** `document_type = INCOME`, предупреждение `TYPE_CONFLICT` уровня `error`
|
||
|
||
### Requirement: Шаг 7 — самопроверки §7
|
||
Шаг 7 SHALL считаться у обоих источников одним словарём `statement_checks.yaml` и выдавать `StatementChecks
|
||
{relations_failed[], soft_relations_failed[]}`; каждое нарушение — `{column_index, relation, codes, expected,
|
||
actual}`, где `codes` — коды цели и членов соотношения. Правила:
|
||
- значение кода в колонке — сумма по всем строкам `ok` с этим кодом; строки `detail`, `unreadable`, `missing` в
|
||
соотношения не входят; нарушение относится ко всем строкам его кодов;
|
||
- соотношение **посчитано**, если непуста цель или хотя бы один член (есть строка `ok` с кодом и `amount` не `null`);
|
||
пусто всё — соотношение пропускается; пустая цель при непустых членах и непустая цель при пустых членах — нарушение;
|
||
- жёсткое соотношение не сошлось, если расхождение больше допуска ±1 в единицах формы **и** больше 0,001 % наибольшего
|
||
по модулю числа соотношения — тогда оно в `relations_failed`; расхождение в пределах округления и мягкие
|
||
соотношения — в `soft_relations_failed`;
|
||
- члены `abs_terms` (2120, 2210, 2220, 2330, 2350, 2410) сравниваются и как записаны, и по модулю; такое соотношение
|
||
подтверждает величину, но не знак (шаг 8);
|
||
- набор соотношений — по `form_variant` (полный или упрощённый); `form_variant = null` — полный. В упрощённом наборе
|
||
соотношения ББ жёсткие, а `2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410` ОФР — мягкое, как `2400` полного набора
|
||
(знак 2410, OQ-12);
|
||
- родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из
|
||
первых четырёх цифр; «сумма подстрок = родитель» (мягкое) — только если хотя бы одна подстрока в колонке непуста.
|
||
|
||
`checks` SHALL быть у каждого `BALANCE` / `INCOME` со строками и отсутствовать у документа без строк.
|
||
`duplicate_line_codes` MUST NOT выдаваться.
|
||
|
||
#### Scenario: «БАЛАНС» без кода
|
||
- **WHEN** строка «БАЛАНС» ушла в `detail` (код потерян), в колонке есть 1100 и 1200
|
||
- **THEN** соотношение `1600 = 1100 + 1200` посчитано и нарушено (пустая цель при непустых членах), в
|
||
`relations_failed`
|
||
|
||
#### Scenario: Лист только с активом
|
||
- **WHEN** в ББ заполнены 1100, 1200, 1600, а 1300–1700 отсутствуют
|
||
- **THEN** `1600 = 1700` нарушено (непустая цель при пустых членах), документ `needs_check`
|
||
|
||
#### Scenario: Повтор кода в соотношении
|
||
- **WHEN** две строки 2410 с суммами 100 и 649
|
||
- **THEN** в соотношениях код 2410 равен 749
|
||
|
||
#### Scenario: Упрощённый ОФР с доходом по налогу
|
||
- **WHEN** у упрощённого ОФР 2410 записан со знаком дохода, и `2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410` по модулю
|
||
не сходится
|
||
- **THEN** нарушение в `soft_relations_failed`, документ из-за него не `needs_check`
|
||
|
||
#### Scenario: Округление итогов
|
||
- **WHEN** `1600 = 1700` расходится на 9,02 при итогах ~17 млрд
|
||
- **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет
|
||
|
||
### Requirement: Шаг 8 — `needs_review` значения
|
||
Сервис SHALL вычислять `needs_review` у каждого значения строк `ok`, `unreadable` и `missing`. У значений строк
|
||
`detail` `needs_review = false` всегда (Р-23): у них нет кода, покрыть их соотношением нельзя, в проверки §7, агрегаты и
|
||
статус документа они не входят; сомнение остаётся видно в подсказке ячейки по `text` и `confidence`. Для остальных
|
||
значений:
|
||
- значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не
|
||
нуждаются;
|
||
- значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое
|
||
посчитано и сошлось: жёсткое — в пределах допуска или округления; мягкое — только в пределах допуска ±1, без
|
||
«округления» (Р-21; так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки — у них жёстких
|
||
соотношений нет);
|
||
- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака совпадает с формой
|
||
строгого большинства остальных членов `abs_terms` этой колонки с непустым `amount`, `is_recognized = true` и без
|
||
правки аналитика (сама ячейка в голосование не входит); форма знака — по результату разбора `parse_amount`: «скобки»
|
||
(пара скобок, в том числе с `}` / `]`), «минус», «без знака»; голосующих нет или равенство голосов — знак не
|
||
подтверждён;
|
||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности;
|
||
- иначе `needs_review = false`.
|
||
|
||
#### Scenario: Догадка подтверждена соотношением (Р-7)
|
||
- **WHEN** у 1230 `"42831 1"` → 428311, `is_recognized = false`, и `1200 = 1210 + 1220 + 1230 + …` в колонке посчитано
|
||
и сошлось
|
||
- **THEN** `needs_review = false`; `text` и уверенность остаются в значении
|
||
|
||
#### Scenario: Знак расхода в фигурной скобке
|
||
- **WHEN** в колонке ОФР 2210 и 2220 записаны в скобках, а 2120 — `"(7744536}"`, и `2100 = 2110 − 2120` сошлось
|
||
- **THEN** у 2120 знак подтверждён, `needs_review = false`
|
||
|
||
#### Scenario: Знак вне конвенции
|
||
- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"38378"` с уверенностью 60 (форма «без знака»), и `2100 = 2110 −
|
||
2120` сходится по модулю
|
||
- **THEN** `needs_review = true` у 2120 независимо от соотношения
|
||
|
||
#### Scenario: Скобка, которая не открывает
|
||
- **WHEN** 2120 — `"{38378)"`
|
||
- **THEN** разбор даёт `amount = null` (шаг 4), значение не покрыто, `needs_review = true`
|
||
|
||
#### Scenario: Расшифровки не держат документ (Р-23, РБ 1 кв. 2025)
|
||
- **WHEN** в ББ и ОФР РБ «Бухгалтерская отчетность 1 кв.25» все неуверенные значения строк с кодом покрыты сошедшимися
|
||
соотношениями, неуверенные коды подтверждены перечнем `fsbu4`, а неуверенные значения остались только у строк `detail`
|
||
(в ОФР — `'1 425 130'` с уверенностью 78, `'134 796'` с уверенностью 71, `'(15 567)'` с уверенностью 75)
|
||
- **THEN** у значений `detail` `needs_review = false`, они статус не держат; документ получает `done`, если выполнены
|
||
остальные условия функции статуса (шапка, период, предупреждения); подсказка у ячеек `detail` показывает `text` и
|
||
уверенность
|
||
|
||
#### Scenario: Справочная строка без проверки
|
||
- **WHEN** значение 2900 с уверенностью 80 (строк 29xx нет ни в одном соотношении)
|
||
- **THEN** `needs_review = true`
|
||
|
||
#### Scenario: Чистая прибыль подтверждена мягким соотношением (Р-21)
|
||
- **WHEN** значение 2400 с уверенностью 66, а `2400 = 2300 − 2410 + 2420` в колонке сошлось с расхождением 0
|
||
- **THEN** у 2400 `needs_review = false`
|
||
|
||
#### Scenario: Мягкое соотношение в пределах округления не подтверждает
|
||
- **WHEN** значение 2400 с уверенностью 66, а `2400 = 2300 − 2410 + 2420` расходится на 9 при итогах ~17 млрд (в пределах
|
||
0,001 %, но больше ±1)
|
||
- **THEN** у 2400 `needs_review = true`; нарушение в `soft_relations_failed`
|
||
|
||
### Requirement: Шаг 8 — подтверждение неуверенного кода структурой
|
||
Код строки SHALL считаться неуверенным, если `line_code_confidence < 100` или при разборе заменён двойник; только к
|
||
неуверенным кодам применяется структурная проверка (Р-12). Данные — упорядоченные перечни кодов по редакциям (`66n`,
|
||
`fsbu4`) и вариантам (`full`, `simplified`), источник — `docs/contracts/statement-code-orders.yaml`; законные повторы
|
||
кода (Р-20) — часть перечня (`repeats`: у `fsbu4` 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО).
|
||
Четырёхзначные коды строк `ok` документа образуют последовательность; коды, которых нет в перечне редакции, в
|
||
последовательность этой редакции не входят. Для **обеих** редакций берётся лучшая подпоследовательность, согласованная
|
||
с порядком перечня варианта формы (итоги после слагаемых; код с повтором может войти столько раз, сколько он стоит в
|
||
перечне); «лучшая» — лексикографически: сначала наибольшее число уверенных кодов (уверенные коды — якоря: включаются
|
||
все, какие можно включить вместе), затем наибольшая длина. Якоря, противоречащие друг другу по порядку бланка, все
|
||
вместе войти не могут — входит наибольшее согласованное их множество; неуверенный код проверяется против выбранного.
|
||
Редакция — с лучшей подпоследовательностью (то же сравнение), при равенстве — по эффективной дате (`fsbu4` после
|
||
2024-12-31). Неуверенный код подтверждён, только если он входит во **все** лучшие подпоследовательности выбранной
|
||
редакции. Вариант формы для перечня — `form_variant` документа (шаг 6, по всем строкам `ok`). Пятизначный код подтверждён, если
|
||
подтверждён его родитель и подстрока стоит после него. Повтор кода, которого нет в перечне редакции (или копий больше,
|
||
чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review = true` у неуверенной копии (Р-16); повтор из
|
||
перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `StatementLine.needs_review =
|
||
true`. У Excel и чистых OCR-кодов `needs_review` строки по структуре MUST NOT ставиться.
|
||
|
||
#### Scenario: Неуверенный код на своём месте
|
||
- **WHEN** код 1230 с уверенностью 50 стоит между 1220 и 1240 (уверенными)
|
||
- **THEN** код подтверждён, `needs_review = false`
|
||
|
||
#### Scenario: Переставленные неуверенные соседи
|
||
- **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240
|
||
- **THEN** у обеих неуверенных строк `needs_review = true`, документ `needs_check`
|
||
|
||
#### Scenario: Якоря противоречат друг другу
|
||
- **WHEN** порядок кодов 1210 (уверенный), 1240 (уверенный), 1230 (уверенный), 1250 (уверенность 50)
|
||
- **THEN** 1240 и 1230 вместе войти не могут; лучшие подпоследовательности — 1210, 1240, 1250 и 1210, 1230, 1250; код 1250
|
||
входит в обе и подтверждён
|
||
|
||
#### Scenario: Законный повтор 1220 (Р-20, AURORA-1431)
|
||
- **WHEN** в ББ печатной формы 1С по ФСБУ 4/2023 стоят подряд 1215, 1220 «НДС по приобретенным ценностям» (уверенность
|
||
100), 1220 «НДС, начисленный по отгрузке» (уверенность 50), 1230
|
||
- **THEN** обе строки 1220 подтверждены структурой (повтор есть в перечне `fsbu4`), `needs_review = false`
|
||
|
||
#### Scenario: Повтор вне перечня
|
||
- **WHEN** в ББ код 1230 стоит дважды подряд, у второй копии уверенность 50
|
||
- **THEN** у второй копии `needs_review = true`, документ `needs_check`
|
||
|
||
#### Scenario: Бланк 66н в 2025 году
|
||
- **WHEN** скан ББ за 3 кв. 2025 на бланке 66н с неуверенным кодом 1120 между 1110 и 1130
|
||
- **THEN** проверяются обе редакции, подпоследовательность `66n` длиннее, код 1120 подтверждён
|
||
|
||
#### Scenario: Нестандартный код Excel
|
||
- **WHEN** в Excel-форме код 1231 или пятизначная подстрока 11101 вне перечня
|
||
- **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра
|
||
|
||
### Requirement: Шаг 8 — поля шапки
|
||
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок с
|
||
уверенной подписью (`label_confidence = 100`; неуверенная подпись дату отчёта не подтверждает, требование «Шаг 2 —
|
||
подпись и дата колонки»); иначе
|
||
поле `report_date` попадает в `needs_review_fields` и выдаётся `REPORT_DATE_UNCERTAIN` (`error`, `field =
|
||
report_date`). У бланка только с относительными подписями колонок абсолютных дат нет, и неуверенная дата отчёта
|
||
структурой не подтверждается никогда: документ `needs_check`, аналитик подтверждает или исправляет дату в ядре (PATCH
|
||
шапки). `needs_review_fields` — поля, которые держат документ в `needs_check`; ИНН и организация с уверенностью ниже 100
|
||
— предупреждения `INN_UNCERTAIN` / `ORGANIZATION_UNCERTAIN` (`info`, с `field`), в `needs_review_fields` не попадают
|
||
(подсветка у поля — по предупреждению); единицы и прочие поля на статус не влияют.
|
||
|
||
#### Scenario: Неуверенная, но верная дата
|
||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок с уверенной подписью 30.09.2025
|
||
- **THEN** `needs_review_fields` пуст, предупреждения о дате нет
|
||
|
||
#### Scenario: Неуверенные дата и подпись не подтверждают друг друга
|
||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, единственная абсолютная подпись колонки «На 30.09.2025» — с
|
||
уверенностью 60
|
||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` и `COLUMN_DATE_UNCERTAIN` уровня `error`;
|
||
снимаются подтверждением даты отчёта и даты колонки аналитиком
|
||
|
||
#### Scenario: Неуверенный год
|
||
- **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
|
||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check`
|
||
|
||
#### Scenario: Бланк 66н с относительными подписями
|
||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, подписи колонок — «На отчетную дату», «На 31 декабря предыдущего
|
||
года»
|
||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`; снимается подтверждением даты
|
||
аналитиком
|
||
|
||
### Requirement: Шаг 8 — предупреждения
|
||
Предупреждение SHALL быть структурой `{code, severity, message, line_refs?, column_index?, field?}`: `severity` —
|
||
`error` | `info`; `line_refs` — список `ref` строк только из `lines[]` (о строках ролей `title` / `header` /
|
||
`numbering` — в `message`); `column_index` — номер столбца сетки (требование «Шаг 2 — номер колонки»); `field` — поле
|
||
шапки. Место в файле — `locator` документа, а не текст. Предупреждения уровня файла (`metrics.warnings`) дополнительно
|
||
имеют `locator?` (json набора ИС, таблица ИС или лист) и SHALL выдаваться там, где документа нет; предупреждение файла
|
||
уровня `error` делает конверт `needs_check` и блокирует `RECOGNIZED` всех документов файла в ядре. Уровни SHALL быть
|
||
такими:
|
||
|
||
| Код | Уровень | Когда |
|
||
|---|---|---|
|
||
| `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе |
|
||
| `TABLE_PARSE_ERROR` | error (файл) | исключение при разборе таблицы ИС (`BACKEND_PARSING_ERROR`); документа нет |
|
||
| `TABLE_LOST` | error (файл) | таблица ИС с содержимым (ячейка с цифрой или «цифрой под видом буквы») без колонки кодов и без `type_hint` |
|
||
| `TABLE_SKIPPED` | info (файл) | таблица ИС без содержимого (только текст), без колонки кодов и без `type_hint`; документа нет |
|
||
| `CODE_COLUMN_NOT_FOUND` | error | у формы с `type_hint` `BALANCE`/`INCOME` нет столбца кодов |
|
||
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) |
|
||
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился |
|
||
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме |
|
||
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента при сопоставлении по порядку не совпали по числу с первым |
|
||
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи |
|
||
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты |
|
||
| `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента |
|
||
| `TYPE_CONFLICT` | error | тип по кодам противоречит `type_hint` |
|
||
| `NO_ORGANIZATION_NO_INN` | error | в шапке нет ни организации, ни ИНН |
|
||
| `PERIOD_UNKNOWN` | error | нет ни даты отчёта, ни абсолютных дат колонок |
|
||
| `REPORT_DATE_UNCERTAIN` | error | неуверенная дата отчёта не совпала с датами колонок |
|
||
| `XLSB_SCALE_SUSPECT` | error | xlsb без форматов при «тыс. руб.» и ≥ 10 % дробных |
|
||
| `INN_NOT_FOUND`, `INN_UNCERTAIN` | info | ИНН не найден или неуверен при найденной организации |
|
||
| `ORGANIZATION_NOT_FOUND`, `ORGANIZATION_UNCERTAIN` | info | организация не найдена или неуверенна при найденном ИНН |
|
||
| `REPORT_DATE_UNPARSED` | info | дата отчёта не разобрана при наличии дат колонок |
|
||
| `UNITS_NOT_FOUND` | info | единицы не найдены |
|
||
| `FILE_TYPE_MISMATCH` | info (файл) | формат файла не совпал с `file_type` запроса |
|
||
| `XLS_NO_CODEPAGE` | info (файл) | книга xls без кодовой страницы — текст мог прочитаться неверно |
|
||
| `COLUMN_SKIPPED` | info | подпись из словаря исключений пропущена |
|
||
| `FRACTIONAL_IN_THOUSANDS` | info | дробные при «тыс. руб.» (кроме xlsb без форматов) |
|
||
| `XLSB_NO_FORMATS`, `NUMBER_FORMAT_UNPARSED` | info | числовые форматы недоступны или не разобраны |
|
||
| `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр |
|
||
| `SHEET_OTHER` | info (файл) | лист Excel «прочее» без `type_hint` и кодов; документа нет |
|
||
| `ROW_SPLIT` | info | разлеплена склеенная строка |
|
||
| `NUMBERS_OUTSIDE_TABLE` | info | числа правее последней подписанной колонки значений не учтены; адреса ячеек в тексте |
|
||
|
||
Предупреждения пути ОСВ — уровня `info`, код с префиксом `OSV_`. Новый код предупреждения вводится словарём пакета и
|
||
таблицей §11 `05-domain-rules.md`; `code` в контракте — строка `^[A-Z][A-Z0-9_]*$`, не перечисление.
|
||
|
||
#### Scenario: Исключение при разборе таблицы ИС
|
||
- **WHEN** разбор второй таблицы json упал с исключением, первая разобрана
|
||
- **THEN** документ первой таблицы со своим статусом; в `metrics.warnings` — `TABLE_PARSE_ERROR` уровня `error` с
|
||
`locator = "file:<имя json>;table:1"`; конверт `needs_check`
|
||
|
||
#### Scenario: НПЗ ОФР без ИНН
|
||
- **WHEN** в шапке ОФР есть организация и нет ИНН
|
||
- **THEN** `INN_NOT_FOUND` уровня `info`, документ может быть `done`
|
||
|
||
#### Scenario: Привязка к ячейке
|
||
- **WHEN** не сошлось `1200 = 1210 + … + 1260` в колонке 1
|
||
- **THEN** нарушение в `checks.relations_failed` с `column_index = 1` и `codes`, `needs_review = true` у ячеек этих
|
||
кодов в колонке 1
|
||
|
||
### Requirement: Шаг 8 — статус документа и конверта
|
||
Статус документа SHALL вычисляться одной функцией общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧
|
||
период определён (дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет
|
||
значений, строк и полей шапки с `needs_review` ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет
|
||
предупреждений документа уровня `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и
|
||
проверках. Вход функции SHALL быть одинаковым у сервиса и ядра и не зависеть от источника: `document_type`,
|
||
`layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть строка из источника,
|
||
`row_index` не `null`), `has_organization_or_inn`, `period_known`, строки `{line_status, needs_review}`, значения
|
||
`{line_status строки, needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни
|
||
предупреждений документа (у ядра — записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки
|
||
и подтверждения аналитика (правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не
|
||
учитываются. Значения строк `detail` функция MUST NOT учитывать, даже если на входе у них `needs_review = true` (Р-23). Статус ОСВ —
|
||
`done`, когда контрольные суммы §1.4 листа сошлись. Статус конверта: `done` ⇔
|
||
все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error`
|
||
(`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований
|
||
со справочником в `done` MUST NOT входить.
|
||
|
||
#### Scenario: Excel-форма без замечаний
|
||
- **WHEN** Excel-ББ с найденной раскладкой, организацией, датой, всеми строками `ok`, сошедшимися соотношениями
|
||
- **THEN** документ `done`; уверенность 100 у Excel выполняет условия покрытия автоматически
|
||
|
||
#### Scenario: Строка без кода с суммой
|
||
- **WHEN** в форме есть строка `unreadable` с суммой
|
||
- **THEN** документ `needs_check`
|
||
|
||
#### Scenario: Ядро считает статус той же функцией
|
||
- **WHEN** ядро после PATCH вызывает функцию статуса по строкам БД формы, у которой все строки `ok`, правленая ячейка
|
||
была `needs_review`, нарушений и предупреждений `error` нет
|
||
- **THEN** функция даёт `done` — тот же результат, что дал бы сервис по тем же данным с подтверждённой ячейкой
|
||
|
||
#### Scenario: Соседний документ не тянет файл
|
||
- **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет
|
||
- **THEN** конверт `needs_check`, у ББ `document.status = done`
|
||
|
||
#### Scenario: Сомнительная ячейка расшифровки
|
||
- **WHEN** у формы все строки `ok` или `detail`, нарушений и `error` нет, единственное значение с уверенностью ниже 100 —
|
||
в строке `detail`
|
||
- **THEN** документ `done`
|
||
|
||
### Requirement: Точка входа пакета для пересчёта по сохранённым данным
|
||
Пакет SHALL предоставлять функцию пересчёта формы по сохранённым данным без сетки и `FormCandidate` — ею ядро
|
||
пересчитывает форму после PATCH и при приёме результата `1.x`, сервисы ею не пользуются. Вход — тип пакета
|
||
`StoredForm`, собираемый хостом из своих данных:
|
||
- `document_type`; `effective_date` (дата отчёта, иначе самая поздняя абсолютная дата колонок — у ядра
|
||
`document.reporting_date`); `has_organization_or_inn`; `period_known`; `needs_review_fields` (как хранит ядро: поле
|
||
уходит из него только правкой или подтверждением даты отчёта — сам пересчёт поля шапки не переоценивает); `accepted`;
|
||
`warnings` — предупреждения документа `{code, severity}` (у ядра — записи `document_issue` вида `warning`);
|
||
- `columns[] {key, column_index, period_date, period_kind}` (`key` — идентификатор хоста, у ядра `column_id`);
|
||
- `lines[] {key, row_index | null, line_status, line_code, line_code_confidence, line_name, code_fixed, parent_key,
|
||
values[] {column_key, amount, text, confidence, is_recognized, value_fixed}}`: `row_index = null` — строка, созданная
|
||
правкой (участвует в §7, как строка `ok`, пока у неё есть сумма); `code_fixed` — код назначен, сменён или подтверждён
|
||
аналитиком (`line_code_modified` или `is_confirmed` строки); `value_fixed` — значение правлено или подтверждено
|
||
(`is_modified` или `is_confirmed`); `text` — сырой текст источника (`source_text`), по нему считается форма знака;
|
||
у значения, созданного правкой, `text = null`.
|
||
|
||
Выход — `{form_variant, edition, checks {relations_failed, soft_relations_failed}, line_needs_review {key → bool},
|
||
value_needs_review {key → bool}, detail_parents {key → parent_key | null}, status}`. Правила — те же шаги 6–8, что у
|
||
сервисов, с поправками: `form_variant` определяется заново по кодам строк `ok` (код мог смениться правкой); `edition` —
|
||
редакция с лучшей подпоследовательностью кодов документа по всем строкам `ok` (как в структурной проверке, при
|
||
равенстве — по `effective_date`; кодов нет — по `effective_date`); `value_fixed` ⇒ `needs_review = false`, такое значение
|
||
в голосование знака не входит; `code_fixed` ⇒ `needs_review` строки = `false`; значения строк `detail` — `false`;
|
||
`accepted` ⇒ жёсткие нарушения статус не держат; `detail_parents` — ближайшая строка `ok` выше по `row_index` (строки
|
||
`row_index = null` родителями не бывают), нет такой — `null`. Пересчёт не меняет `line_status`, суммы и коды — только
|
||
выводит признаки.
|
||
|
||
#### Scenario: Пересчёт после правки по строкам БД
|
||
- **WHEN** ядро передаёт форму: ячейка 1230 правлена (`value_fixed`), код 1250 назначен строке бывшей `unreadable`
|
||
(`code_fixed`), шаблонная строка 1260 с суммой (`row_index = null`), строка `detail` с уверенностью 60
|
||
- **THEN** `checks` посчитаны с 1250 и 1260 в `1200 = …`; у 1230 и у значения `detail` `needs_review = false`, у строки
|
||
1250 `needs_review = false`; `status` — по общей функции статуса; `form_variant` и `edition` — по кодам после правки
|
||
|
||
#### Scenario: Смена кода меняет вариант
|
||
- **WHEN** у упрощённого по структуре ББ (есть 1600, нет 1100 и 1200) аналитик назначает строке код 1100
|
||
- **THEN** точка входа возвращает `form_variant = full`, нарушения считаются полным набором
|