218 lines
24 KiB
Markdown
218 lines
24 KiB
Markdown
# statement-parsing Specification
|
||
|
||
## Purpose
|
||
Разбор бухгалтерского баланса (0710001) и отчёта о финансовых результатах (0710002) из книг Excel в
|
||
`statementDocument` контракта v1.2 по §12 `05-domain-rules.md`: тип формы, шапка, колонки-периоды, строки с кодами и
|
||
значения, соотношения §7, склейка листов-продолжений внутри книги. Структура ищется по содержимому: ни буквы колонок,
|
||
ни номера строк, ни имена листов не являются контрактом; синонимы и соотношения — данные в `rules/statement_*.yaml`.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Листы форм и их продолжения находятся по содержимому
|
||
Классификация листа SHALL различать `OSV`, `STATEMENT`, `STATEMENT_CONTINUATION` и `OTHER` и выполняться в порядке:
|
||
признак ОСВ → форма → продолжение → прочее. Признак ОСВ SHALL проверяться первым. Лист SHALL считаться `STATEMENT`, если
|
||
в любой строке до первой строки заголовка таблицы формы или первой строки с кодом формы есть заголовок формы или код
|
||
ОКУД из словаря заголовков пакета `arm_statements`. Лист без заголовка формы SHALL считаться `STATEMENT_CONTINUATION`,
|
||
если на нём есть маркер продолжения («Форма 0710001 с.2»), либо строка заголовка таблицы формы (ячейка «Код» и правее
|
||
ячейки-периоды), либо колонка, в которой не меньше 60 % непустых значений — коды строк форм; слабое продолжение (столбец
|
||
кодов найден только ослабленным поиском пакета, не меньше `min_codes_continuation` кодов) SHALL учитываться только сразу
|
||
после листа формы. Лист без признаков ОСВ, формы и продолжения, без `type_hint` и кодов SHALL считаться `OTHER`: документа
|
||
нет, предупреждение файла `SHEET_OTHER` (`info`, `locator = sheet:<имя>`). Тип формы (`type_hint`) SHALL определяться
|
||
правилом пакета: ОКУД по словарю (0710001 → `BALANCE`, 0710002 → `INCOME`, прочие 0710xxx → `DISCLOSURE`), иначе заголовок
|
||
по словарю («Пояснения», «Расшифровка» → `DISCLOSURE`), иначе `null`. Титул «Бухгалтерская отчетность» MUST NOT задавать
|
||
тип: тип берётся из ближайшего следующего заголовка. Два заголовка форм на одном листе SHALL давать два блока и два
|
||
документа, второй — со строки своего заголовка.
|
||
|
||
#### Scenario: Заголовок формы после титульного блока
|
||
- **WHEN** разбирается `statements-xlsx/npz/2025-Q3/Бухгалтерская отчетность … .xlsx`, лист «Бухгалтерский баланс. Лист 1»,
|
||
где заголовок «Бухгалтерский баланс» стоит ниже титула «Бухгалтерская отчетность за январь - сентябрь 2025 г.»
|
||
- **THEN** лист классифицирован как `STATEMENT`, документ `BALANCE`, `header.okud = "0710001"`
|
||
|
||
#### Scenario: Лист-продолжение без заголовка
|
||
- **WHEN** разбирается лист «Бухгалтерский баланс. Лист 2» той же книги (первая строка — «в том числе:», далее 11901, 1100…)
|
||
- **THEN** лист классифицирован как `STATEMENT_CONTINUATION` и не даёт отдельного документа
|
||
|
||
#### Scenario: Маркер «с.2» в 1С-выгрузке
|
||
- **WHEN** разбирается `statements-xlsx/unagrande-co/2024-FY/Б.Б. Лист 2.xls` (первая строка «Форма 0710001 с.2»)
|
||
- **THEN** лист классифицирован как `STATEMENT_CONTINUATION`, тип по ОКУД маркера — `BALANCE`
|
||
|
||
#### Scenario: Две формы на одном листе
|
||
- **WHEN** разбирается синтетический лист `st01_both_forms_one_sheet` (баланс, затем ОФР)
|
||
- **THEN** результат содержит два документа — `BALANCE` и `INCOME` с одинаковым `locator` листа, строки ОФР не попали в баланс
|
||
|
||
#### Scenario: Расшифровка
|
||
- **WHEN** разбирается лист с заголовком «Пояснения к бухгалтерскому балансу и отчёту о финансовых результатах»
|
||
- **THEN** документ `DISCLOSURE` с `locator`, без `columns` и `lines`, со статусом `done`; статус книги от него не зависит
|
||
|
||
#### Scenario: Лист «прочее»
|
||
- **WHEN** в книге лист «Примечания» без заголовка формы, ОКУД и кодов
|
||
- **THEN** документа нет; в `metrics.warnings` — `SHEET_OTHER` уровня `info` с `locator = "sheet:Примечания"`
|
||
|
||
### Requirement: Шапка документа
|
||
Адаптер SHALL собирать шапку каждого блока формы в границах этого блока: поля `organization_name`, `inn`, `report_date`,
|
||
`units`, `okud`, `okpo`, `okopf`, `okfs`, `okei`, `address` — каждое `Cell` или `null`, найденные поля с `confidence =
|
||
100`. Метки поля SHALL искаться по словарю заголовков пакета в области от начала блока до строки заголовка таблицы
|
||
(у блока без кодов — во всём блоке); значение — ближайшая непустая ячейка правее метки, иначе первая непустая ячейка
|
||
следующей строки. ИНН SHALL браться у метки «ИНН», причём строки после метки аудитора MUST NOT использоваться.
|
||
`organization_name` SHALL браться после метки «Организация», а при пустом значении — из ближайшей строки выше с признаком
|
||
ОПФ. Разбор текста `Cell` (ИНН — только цифры длиной 10 или 12, дата отчёта по словарю периодов, единицы по словарю
|
||
единиц или коду ОКЕИ) SHALL выполнять общий код пакета; единицы MUST NOT нормализоваться адаптером. Шапка другого блока
|
||
или другого листа MUST NOT использоваться. У документа без шапки все поля SHALL быть `null`, кроме `form_name`, который
|
||
ставит пакет по заголовку.
|
||
|
||
#### Scenario: Полная шапка 1С
|
||
- **WHEN** разбирается `unagrande-co/2024-FY/Б.Б Лист 1.xls`
|
||
- **THEN** `organization_name = 'Акционерное общество "Унагранде Компани"'`, `inn = "3249004697"` (не ИНН аудитора
|
||
7716021332), `okpo = "47888105"`, `okud = "0710001"`, `units = "в тыс. рублей"`, `report_date = 2024-12-31`,
|
||
`report_date_text = "на 31 декабря 2024 г."`, `address` начинается с «242440»; `*_confidence = 100`
|
||
|
||
#### Scenario: Организация над меткой, ОФР за период
|
||
- **WHEN** разбирается лист «Отчет о финансовых результата» книги НПЗ
|
||
- **THEN** `organization_name = 'ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"'`, `inn = null` (на листе ОФР метки «ИНН» нет, а
|
||
шапка другого листа не используется), `okpo = "16098811"`, `report_date = 2025-09-30`, `units = "тыс. руб."`; у
|
||
документа `BALANCE` той же книги `inn = "2315995868"` из титульного блока над заголовком формы
|
||
|
||
#### Scenario: Дата из тройки чисел
|
||
- **WHEN** разбирается `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` (ячейки 30 | 9 | 2025 как числа)
|
||
- **THEN** `report_date = 2025-09-30`, `inn = "7016006635"`, `okpo = "85262693"`
|
||
|
||
### Requirement: Склейка листов-продолжений и статус документа
|
||
Листы книги SHALL обрабатываться по порядку; лист-продолжение SHALL дописываться в сетку открытой формы адаптером до
|
||
раскладки, граница листа — в `segment_breaks`. Открытая форма — последний кандидат с `type_hint` `BALANCE` или `INCOME`.
|
||
Лист с маркером «Форма … с.N» присоединяется по типу маркера; расхождение типа маркера с `type_hint` формы SHALL давать
|
||
отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT` (`error`). Лист без маркера и шапки SHALL
|
||
присоединяться, только если его коды в диапазоне открытой формы и, при трёх и более кодах, число колонок сумм совпадает с
|
||
формой; иначе — отдельный кандидат и предупреждение `SHEET_NOT_JOINED` (`error`). `locator` SHALL перечислять имена листов
|
||
через запятую. Продолжение без открытой формы SHALL давать отдельный кандидат без предупреждения адаптера (пакет выдаёт
|
||
`NO_ORGANIZATION_NO_INN`), документ — `needs_check`. Статус документа SHALL вычислять функция пакета (одна для сервисов и
|
||
ядра); `checks` и условие `done` — по спеке `statement-normalization`. `metrics.rows` книги SHALL быть суммой строк данных
|
||
ОСВ (без `TOTAL`) и строк `ok` документов форм (строки `detail` и без кода — отдельно). Если книга завершилась ошибкой
|
||
листа ОСВ, ответ — ошибка этого листа; список неразобранных листов форм не добавляется.
|
||
|
||
#### Scenario: Баланс на пяти листах
|
||
- **WHEN** разбирается книга НПЗ
|
||
- **THEN** два документа: `BALANCE` с `locator = "sheet:Бухгалтерский баланс. Лист 1,Бухгалтерский баланс. Лист 2,…,Лист 5"`,
|
||
одной сеткой и `segment_breaks` по границам листов, строки от 1105 до 1700 включительно, `INCOME` на трёх листах (лист
|
||
отчёта с единственной строкой 2400 присоединяется как продолжение), строка 2400 присутствует; `1600 = 1700` и
|
||
`2100`/`2200`/`2300` — в `soft_relations_failed` как округление (OQ-138); статус книги `done`
|
||
|
||
#### Scenario: Лист-продолжение с одной кодовой строкой
|
||
- **WHEN** **сразу** после листа формы идёт лист без шапки с единственной строкой 2400 (код в диапазоне формы)
|
||
- **THEN** лист присоединяется к открытой форме (кодов меньше трёх — число колонок не сверяется), строка присутствует,
|
||
граница листа — в `segment_breaks`
|
||
|
||
#### Scenario: Лист другой ширины с тремя кодами
|
||
- **WHEN** лист без маркера и шапки содержит три кода ББ и четыре колонки сумм при трёх у открытой формы
|
||
- **THEN** лист — отдельный кандидат, предупреждение `SHEET_NOT_JOINED` уровня `error`
|
||
|
||
#### Scenario: Тип маркера против формы
|
||
- **WHEN** после листа ОФР идёт лист с маркером «Форма 0710001 с.2»
|
||
- **THEN** лист — отдельный кандидат `BALANCE`, предупреждение `CONTINUATION_TYPE_CONFLICT` уровня `error`
|
||
|
||
#### Scenario: Одинокая страница 2
|
||
- **WHEN** разбирается `unagrande-group/2024-FY/ОФР. Лист 2.xls` (стр. 1 в пакете нет)
|
||
- **THEN** один документ `INCOME` со строками 2510–2910, пустой шапкой, статус `needs_check`
|
||
|
||
#### Scenario: Страница 1 без продолжения
|
||
- **WHEN** разбирается `unagrande-co/2024-FY/Б.Б Лист 1.xls` (только актив)
|
||
- **THEN** документ `BALANCE` со строками 1110–1600, шапка полная, `1600 = 1100 + 1200` выполнено, `1600 = 1700`
|
||
нарушено как «непустая цель при пустых членах» → `needs_check`
|
||
|
||
#### Scenario: Смесь ОСВ и форм в одной книге
|
||
- **WHEN** разбирается синтетическая книга `st04_mixed_book` (лист ОСВ 60 и лист баланса)
|
||
- **THEN** результат содержит `osvDocument` и `statementDocument`, статус книги по обоим
|
||
|
||
### Requirement: Словари форм, координаты, golden
|
||
Словари форм (заголовки и ОКУД, периоды, соотношения §7, единицы, перечни кодов) SHALL жить в пакете `arm_statements` и в
|
||
tabular-parser MUST NOT дублироваться: файлы `api/rules/statement_*.yaml` удалены. Код `src/parser/statements/**` и
|
||
`src/parser/book.py` MUST NOT содержать литералов координат (линтер `test_no_coordinates`); `ref` ячейки собирается из
|
||
индексов функцией `grid.cell_ref`. Снимки `../fixtures/golden/expected_statement_parse/**` SHALL покрывать все файлы
|
||
`statements-xlsx/**` и обновляться только `make golden-update`; `service.statements_version` — волатильное поле снимка.
|
||
Property-тест по манифесту `tests/statements_manifest.yaml` SHALL проверять тип, ИНН, ожидаемый статус, сходимость
|
||
жёстких соотношений для полных форм и, где задано, множество кодов предупреждений `error` документа.
|
||
|
||
#### Scenario: Новый синоним без релиза кода
|
||
- **WHEN** в словарь заголовков пакета `arm-statements` добавлен синоним заголовка «Баланс предприятия» и копия пакета
|
||
обновлена скриптом `sync-statements`
|
||
- **THEN** лист классифицирован как `BALANCE` без изменения кода tabular-parser; собственного словаря форм в сервисе нет
|
||
|
||
#### Scenario: Мёртвая запись словаря
|
||
- **WHEN** запись словаря форм не срабатывает ни на одном файле
|
||
- **THEN** это проверяет тест покрытия словарей пакета `arm-statements` (внутри пакета, на его данных); тест покрытия
|
||
tabular-parser словари форм не охватывает
|
||
|
||
#### Scenario: Регрессия по снимкам
|
||
- **WHEN** результат разбора любого файла `statements-xlsx/**` отличается от снимка
|
||
- **THEN** golden-тест падает с diff; обновление — только `make golden-update` и отдельный коммит в fixtures
|
||
|
||
### Requirement: Адаптер Excel шага 1
|
||
tabular-parser SHALL оставить у себя чтение книги (лимиты `PARSE_MAX_BYTES`, `PARSE_TIMEOUT_SECONDS`, формат по
|
||
сигнатуре, лимиты zip и объединённых ячеек, `UNREADABLE`), классификацию листов (ОСВ — отдельный путь; форма;
|
||
продолжение; прочее) и адаптер «блок формы на листе → `FormCandidate`» общего кода; шаги 2–8 по формам SHALL
|
||
выполняться копией пакета `arm_statements`. Адаптер SHALL: выделять блоки форм на листе по заголовку или ОКУД (без
|
||
кодов между ними блоки сливаются, заголовок после кодов — новая форма); собирать шапку каждого блока в его границах
|
||
(организация, ИНН без ИНН аудитора, ОКПО, ОКОПФ, ОКФС, единицы, дата, адрес) как `Cell` с `confidence = 100`;
|
||
заполнять у каждой ячейки `text` по числовому формату (tp#42) и `value`, `ref = <лист>!<A1>` (имя листа в одинарных
|
||
кавычках по правилу Excel, если в нём есть что-то кроме букв, цифр и `_`); дописывать
|
||
листы-продолжения в сетку открытой формы до раскладки (маркер «Форма … с.2» или позиция листа; проверки типа,
|
||
диапазона кодов и, при трёх и более кодах, числа колонок — как `join_blocker`), записывая границы листов в
|
||
`segment_breaks`. Несовпадение формата с `file_type` — предупреждение файла `FILE_TYPE_MISMATCH` (`info`). Лист без
|
||
признаков ОСВ и формы, без `type_hint` и кодов — предупреждение файла `SHEET_OTHER` (`info`, `locator = sheet:<имя>`),
|
||
документа нет.
|
||
|
||
#### Scenario: Книга ББ + ОФР
|
||
- **WHEN** книга содержит лист «Баланс» (ОКУД 0710001), лист «Баланс (2)» с маркером «Форма 0710001 с.2» и лист «ОФР»
|
||
- **THEN** адаптер выдаёт два кандидата: `BALANCE` с `locator = sheet:Баланс,Баланс (2)` и одной сеткой из двух листов
|
||
(`segment_breaks` — первая строка второго листа) и `INCOME` с `locator = sheet:ОФР`
|
||
|
||
#### Scenario: Ссылки ячеек
|
||
- **WHEN** код строки стоит в ячейке C27 листа «Баланс»
|
||
- **THEN** `ref` строки — `Баланс!C27`; у листа «Бухгалтерский баланс. Лист 1» — `'Бухгалтерский баланс. Лист 1'!C27`
|
||
|
||
### Requirement: Ячейка Excel
|
||
Адаптер SHALL превращать ячейку Excel в `Cell`: `text` — как показывает числовой формат ячейки (tp#42), `value` — число
|
||
или дата, если источник их знает, `confidence = 100`, `ref = <лист>!<A1>` по правилу Excel (имя листа в одинарных
|
||
кавычках, если в нём есть что-то кроме букв, цифр и `_` или оно начинается с цифры; апостроф внутри имени удваивается).
|
||
Сетка кандидата — плотная: строки блока целиком, пустая позиция — `null`; объединённая ячейка — только в позиции левой
|
||
верхней. Значения ББ/ОФР из Excel SHALL приниматься пакетом по `value`; ОСВ числовой формат не применяет.
|
||
|
||
#### 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"`
|
||
|
||
### Requirement: Строки без кода и неизвестные колонки в Excel
|
||
tabular-parser SHALL отдавать строки без кода с содержимым со статусами `detail` / `missing` / `unreadable` (Р-8; ранее
|
||
не отдавались, OQ-36 / OQ-141), числовые колонки с нераспознанной подписью — колонкой без даты и `error` (ранее
|
||
пропускались), повторы кодов — всеми строками без `duplicate_line_codes`.
|
||
|
||
#### Scenario: НПЗ «Реализация иного имущества»
|
||
- **WHEN** в ОФР ФСБУ 4/2023 строка «Реализация иного имущества…» без кода с суммами стоит под строкой 2340
|
||
- **THEN** в результате строка `detail` с `parent_ref` строки 2340; ранее она отбрасывалась со счётчиком
|
||
|
||
### Requirement: Манифест ожиданий на реальных формах
|
||
Манифест `tabular-parser/api/tests/statements_manifest.yaml` SHALL быть обновлён в PR перехода на 2.0 по правилу
|
||
«посчитанного» соотношения §7 (пустая цель при непустых членах — нарушение): `failed_relations` страниц 2 баланса без
|
||
1600 получают `["1600 = 1700"]` — `unagrande-co/2024-FY/Б.Б. Лист 2.xls`, `unagrande-group/2024-FY/ББ. Лист 2 (2).xls`,
|
||
`unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls` (было `[]`, причина «страница 2 без шапки» остаётся); страницы 1 «только
|
||
актив» (`Б.Б Лист 1.xls`, `ББ. Лист 1 .xls`, `ББ. Лист 1 3кв.25.xls`) сохраняют `["1600 = 1700"]` (теперь как «непустая
|
||
цель при пустых членах»). Статусы `done` (Межениновская ББ и ОФР, НПЗ ББ и ОФР, ОФР «Лист 1» Унагранде ×2) MUST NOT
|
||
меняться: их строки без кода уходят в `detail`, строки шапки выше заголовка — в `title`. Остальные записи манифеста
|
||
пересчитываются прогоном и правятся только с объяснением в описании PR.
|
||
|
||
#### Scenario: Страница 2 баланса
|
||
- **WHEN** разбирается `unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls` (есть 1700, нет 1600)
|
||
- **THEN** документ `needs_check`, в `checks.relations_failed` — `1600 = 1700` с пустой целью
|
||
|
||
### Requirement: Статус книги
|
||
Статус конверта tabular-parser SHALL вычисляться по общему правилу: `done` ⇔ все документы `done` ∧ нет предупреждений
|
||
файла уровня `error`; книга без листов ОСВ и форм — `error INVALID_DOCUMENT`. Порядок документов — сначала ОСВ в
|
||
порядке листов, затем формы.
|
||
|
||
#### Scenario: Книга только из пояснений
|
||
- **WHEN** книга содержит только листы пояснений (0710003, «Пояснения»)
|
||
- **THEN** документы `DISCLOSURE` со статусом `done`, конверт `done`
|