openspec: спека Среза 2b — парсер ББ и ОФР из Excel (statement-parsing), контракт v1.2
Proposal, design (D-1…D-11, решения без обсуждения №1–№11), новая capability statement-parsing, дельта tabular-recognition-service (BookParser, schema_version 1.2), tasks. Решения владельца 06.10.2026: только парсер и контракт; страницы-продолжения без склейки между файлами; единицы как есть; пятизначные подстроки как строки; контракт MINOR 1.2; без запасного пути по именам. Refs #14 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
parent
e3eeda8d84
commit
76b2b36f21
213
openspec/changes/slice-2b-statement-parser/design.md
Normal file
213
openspec/changes/slice-2b-statement-parser/design.md
Normal file
@ -0,0 +1,213 @@
|
||||
# Design: Срез 2b — парсер ББ и ОФР из Excel
|
||||
|
||||
## Context
|
||||
|
||||
- Точка подключения прежняя: `get_parser(settings)` → `Parser.parse(data, file_type) -> ParseOutcome`; воркер
|
||||
(`core/worker.py`) вызывает парсер в `asyncio.to_thread`, собирает конверт и пишет `results/{file_id}/{job_id}/result.json`.
|
||||
`ParseOutcome` требует непустые `documents` при `done|needs_check`.
|
||||
- Сейчас `OsvParser.parse` делает и оркестрацию книги (лимит, чтение, дедлайн, `classify_sheet`, цикл по листам), и
|
||||
конвейер листа ОСВ (`parse_sheet`). Листы `SheetKind.STATEMENT` пропускаются с предупреждением.
|
||||
- Контракт v1.1 уже содержит `statementDocument{document_type BALANCE|INCOME|OTHER, locator, header{form_name,
|
||||
organization_name, inn, report_date_text, report_date, units, okud, okpo, okopf, okfs, address, *_confidence},
|
||||
columns[{column_index, label, period_date}], lines[{line_code, line_name, *_confidence, values[{column_index, amount,
|
||||
string_value, confidence, is_recognized}]}]}` — поля «как у OCR». Табличный путь заполняет `confidence = 100`,
|
||||
`is_recognized = true` (§12).
|
||||
- Реальные файлы (`../fixtures/statements-xlsx/**`, 15 файлов + 4 шаблона), три раскладки:
|
||||
1. **1С-xls постранично** (Унагранде Компани, Унагранде Групп): один файл = одна страница формы; на странице 1 титульный
|
||||
блок (организация, ИНН, ОКПО, ОКВЭД, ОКОПФ/ОКФС, единица «в тыс. рублей», ОКЕИ 384, адрес, аудитор с **своим** ИНН),
|
||||
заголовок таблицы «Пояснения | Наименование показателя | Код | На 31 декабря 2024 г. | … 2023 | … 2022»; на странице 2
|
||||
только «Форма 0710001 с.2» и та же строка заголовка таблицы. Под строками 1110/1150/1540 — пятизначные подстроки
|
||||
11101, 11102 (1С «в том числе» без слов), есть строки без кода и без имени с нулями. Значения — рубли с копейками при
|
||||
единице «тыс. руб.».
|
||||
2. **Многолистовая xlsx ФСБУ 4/2023** (НПЗ): ББ на 5 листах, ОФР на 3; на первом листе титул «Бухгалтерская отчетность
|
||||
за январь - сентябрь 2025 г.» и ниже заголовок «Бухгалтерский баланс | на 30 сентября 2025 г. | 0710001»; листы 2–5
|
||||
начинаются сразу со строк («в том числе:», 11901, 1100, …) без заголовка; есть строка 1105 «Гудвил», подстроки
|
||||
12301…12314, 23201…, в ОФР строка без кода с суммами («Реализация иного имущества…»).
|
||||
3. **`TDSheet`** (Межениновская): вся форма на одном листе, заголовок «БУХГАЛТЕРСКИЙ БАЛАНС / за 9 месяцев 2025 г.»,
|
||||
дата тройкой «30 | 9 | 2025» (числа, не строки), колонки «Наименование показателя | Коды | За 9 месяцев\n2025 года |
|
||||
На 31 декабря\n2024 года | …», под шапкой строка нумерации «1 | 2 | 3 | 4 | 5», значения — тысячи с дробной частью.
|
||||
4. **Шаблоны** 66н и ФСБУ 4/2023: заголовок таблицы разнесён на две строки («На» / «20__ г.»), коды есть, сумм нет,
|
||||
лист содержит обе страницы формы с маркером «Форма 0710001 с. 2» посередине; в шаблоне ФСБУ строка «АКТИВ» и код 1110
|
||||
стоят в одной строке из-за объединений (образец формы, не данные).
|
||||
- Параллельно в ветках issues #9/#10/#11 правятся модули ОСВ (`header.py`, `classify.py`, `checks.py`, `hierarchy.py`,
|
||||
словари ОСВ, golden ОСВ). Этот change их не трогает.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** все 15 реальных файлов разбираются без правил под файл, организацию, имя листа или номер строки; полные формы
|
||||
(НПЗ, Межениновская) дают `done` с сошедшимися жёсткими соотношениями §7; постраничные xls дают документы с данными и
|
||||
честный `needs_check` для страниц без шапки; формулы §7 и синонимы — данные; конвейер форм изолирован от ОСВ так, что
|
||||
параллельные правки ОСВ не конфликтуют; контракт растёт только MINOR-ом. **Non-Goals:** см. proposal.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D-1. `BookParser` — оркестратор книги, `OsvParser` — конвейер листа
|
||||
`book.py`: `BookParser(osv: OsvParser, statements: StatementParser, rules: Rules, max_bytes, timeout_seconds, clock)`.
|
||||
`parse()` повторяет нынешний `OsvParser.parse`/`_parse_book`: лимит `PARSE_MAX_BYTES` → `read_book` → `Deadline` → для
|
||||
каждого листа `classify_sheet` → `OSV` в `osv.parse_sheet`, `STATEMENT`/`STATEMENT_CONTINUATION` накапливаются и
|
||||
передаются `statements.parse_sheets(sheets_in_order, deadline)` **одним вызовом** (склейка продолжений требует видеть
|
||||
соседние листы), `OTHER` → предупреждение. Документы книги — ОСВ в порядке листов, затем формы в порядке листов. Статус
|
||||
`done`, если все документы `ok`; ни одного документа ОСВ или формы → `InvalidDocumentError` («в книге нет листов ОСВ или
|
||||
форм»). `OsvParser` теряет `parse`/`_parse_book`, оставляет `parse_sheet` и свойства; `get_parser` собирает `BookParser`.
|
||||
`NullParser` без изменений. *Альтернативы:* ветка внутри `osv.py` (смешение доменов, конфликты с #9–#11); два
|
||||
`Parser`-а с композитом (книга читается дважды, дедлайн и лимиты делятся). *Цена ошибки:* `BookParser` тонкий,
|
||||
переносится.
|
||||
|
||||
### D-2. Классификация листа — три вида для форм, признаки по области до таблицы
|
||||
`detect.classify_sheet(sheet, rules) -> SheetKind` расширяется значением `STATEMENT_CONTINUATION`. Порядок проверки
|
||||
прежний: признак ОСВ → `OSV`; иначе признак формы (заголовок формы или ОКУД из `statement_headers.yaml`) в любой строке
|
||||
**до** первой строки, похожей на заголовок таблицы форм («Код»/«Коды» и справа ячейки-периоды) либо до первой строки с
|
||||
кодом строки формы → `STATEMENT`; иначе маркер продолжения («Форма 0710001 с.2» / «с. 2») **или** строка заголовка
|
||||
таблицы форм без заголовка формы **или** (без заголовка таблицы) колонка, где ≥ 60 % непустых значений — коды форм, →
|
||||
`STATEMENT_CONTINUATION`; иначе `OTHER`. Ограничение «первые N строк» для признаков формы снимается: у НПЗ заголовок
|
||||
формы стоит после титульного блока. Признак ОСВ по-прежнему ищется там же, где сейчас (модуль ОСВ не меняется).
|
||||
|
||||
### D-3. Тип формы и разрез листа
|
||||
`statements/detect.py`: `find_forms(sheet, rules) -> list[FormBlock(kind: BALANCE|INCOME|DISCLOSURE, title_row,
|
||||
okud, table_header_row, first_data_row, last_row)]`. Тип по заголовку/ОКУД из `statement_headers.yaml`:
|
||||
«Бухгалтерский баланс» / 0710001 → BALANCE; «Отчет о финансовых результатах», «Отчет о прибылях и убытках» / 0710002 →
|
||||
INCOME; «Пояснения к бухгалтерскому балансу», «Расшифровка», «Отчет об изменениях капитала» (0710003), «Отчет о
|
||||
движении денежных средств» (0710004) и прочие ОКУД 0710xxx → DISCLOSURE. Титул «Бухгалтерская отчетность» — не тип: тип
|
||||
берёт ближайший следующий заголовок. Два заголовка форм на листе → два блока, второй начинается со своего заголовка
|
||||
(§12 п. 1). Если ОКУД и текст заголовка противоречат — предупреждение, тип по ОКУД. DISCLOSURE → документ с `locator`,
|
||||
шапкой (если нашлась) и пустыми `columns`/`lines`; на статус книги не влияет.
|
||||
|
||||
### D-4. Заголовок таблицы, колонка кодов, колонка наименований, колонки-периоды
|
||||
`statements/columns.py`. Строка заголовка таблицы — первая после заголовка формы строка, где есть ячейка «Код»/«Коды»
|
||||
(синонимы в словаре) и правее неё хотя бы одна ячейка, матчащая период из `statement_periods.yaml`; при отсутствии
|
||||
слова «Код» — строка, под которой начинается колонка кодов. Заголовок, разнесённый на две строки (шаблоны: «На» +
|
||||
«20__ г.»; Межениновская: перенос строки внутри ячейки), склеивается: текст ячейки и текст ячейки той же колонки в
|
||||
следующей строке, если та строка не содержит кодов и сумм. Строка нумерации колонок («1 2 3 4 5» подряд) пропускается.
|
||||
Колонка кодов: среди колонок между наименованием и первой колонкой-периодом та, где ≥ 60 % непустых значений в блоке —
|
||||
коды форм (`^\d{4}$` или `^\d{5}$` в диапазонах 1100–1799 / 2100–2999 и их пятизначных расширениях, плюс 5xxx для
|
||||
расшифровок); значения читаются как текст (числа 1110.0 → «1110»). Нет такой колонки → документ без строк, `needs_check`,
|
||||
предупреждение «колонка кодов не найдена» (решение владельца 06.10.2026, вопрос 6). Колонка наименований — ближайшая
|
||||
левее кодов колонка с преобладанием текста; колонка «Пояснения» исключается по заголовку и по тому, что её значения —
|
||||
пустые или короткие (≤ 3 символов). Колонки-периоды — все ячейки заголовка правее кодов с распознанным периодом, по
|
||||
порядку слева направо: `column_index` 0, 1, 2…; `period_date` — дата («На 31 декабря 2024 г.», «31.12.2024») или
|
||||
конец периода («За 9 месяцев 2025 г.» → 2025-09-30, «За январь - сентябрь 2025 г.» → 2025-09-30, «За 2024 г.» →
|
||||
2024-12-31). Ячейки заголовка правее кодов без периода («Изменение», «%») не становятся колонками, их значения не
|
||||
читаются, одно предупреждение на документ. `is_current` в контракте нет — ядро берёт самую позднюю дату.
|
||||
|
||||
### D-5. Шапка документа
|
||||
`statements/header.py`: область — от начала блока формы до строки заголовка таблицы; для первого блока на листе — от
|
||||
начала листа (титульный блок НПЗ над заголовком формы входит в шапку баланса); шапка другого листа MUST NOT
|
||||
использоваться (у ОФР НПЗ нет метки «ИНН» → `inn = null`). Метки и регулярки — в
|
||||
`statement_headers.yaml`; значение метки — ближайшая непустая ячейка правее в той же строке, иначе первая непустая ячейка
|
||||
следующей строки (адрес, имя организации под меткой). Особые правила: `inn` — 10 или 12 цифр у метки «ИНН», строки после
|
||||
метки «аудитор» исключаются; `organization_name` — после «Организация», а если справа пусто и строкой выше есть текст с
|
||||
признаком ОПФ (ООО/АО/ПАО/ЗАО/кавычки) — он (НПЗ); `okopf`/`okfs` — у общей метки «по ОКОПФ / ОКФС» два числа по порядку;
|
||||
`units` — текст после «Единица измерения» как есть, ОКЕИ 384/385 рядом только подтверждает; `report_date_text` — подзаголовок
|
||||
формы («на 31 декабря 2024 г.», «за 9 месяцев 2025 г.», «за январь - сентябрь 2025 г.»), `report_date` — его дата или конец
|
||||
периода; при отсутствии — тройка «число, месяц, год» у метки «Дата»/«Отчетная дата» (строки или числа). Для ОФР дата отчёта
|
||||
— конец периода. `*_confidence = 100` там, где поле найдено, `null` — где нет. Блок продолжения без шапки → все поля
|
||||
`null`, `form_name` из маркера «Форма 0710001 с.2» (тип по ОКУД в маркере).
|
||||
|
||||
### D-6. Строки и значения
|
||||
`statements/lines.py`: строка блока с кодом в колонке кодов → `StatementLine(line_code=<текст без пробелов>,
|
||||
line_name=<trim, переносы → пробел>, line_code_confidence=100, line_name_confidence=100)`; строки без кода (разделы,
|
||||
«в том числе:», пустые с нулями, строка «Реализация иного имущества…» у НПЗ) не отдаются (`ASSUMPTION(OQ-36)`), их число —
|
||||
в предупреждение «строк без кода пропущено: N» только если среди них есть суммы. Пятизначные подстроки отдаются как
|
||||
обычные строки (решение владельца, вопрос 4). Повтор кода внутри документа → остаётся первая строка, код в
|
||||
`checks.duplicate_line_codes`, `needs_check`. Значения: для каждой колонки-периода `StatementValue(column_index, amount,
|
||||
string_value, confidence=100, is_recognized)`: число → `amount` через `numbers.parse_money` (Decimal, 2 знака, скобки →
|
||||
минус, пробелы/NBSP убираются), `is_recognized = true`; пусто/«–»/«-» → `amount = null`, `string_value = null`,
|
||||
`is_recognized = true`; нечисловой текст → `amount = null`, `string_value = <текст>`, `is_recognized = false`. Единицы не
|
||||
нормализуются: если `header.units` содержит «тыс.» и хотя бы у 10 % значений дробная часть ≠ 0, в предупреждения пишется
|
||||
«N значений с дробной частью при единице тыс. руб.» (решение владельца, вопрос 3; новый OQ в docs).
|
||||
|
||||
### D-7. Соотношения §7 — данные в `statement_checks.yaml`
|
||||
Формат записи: `{form: BALANCE|INCOME, relation: "1600 = 1100 + 1200", strict: true|false, abs_terms: [2120, 2210, …]}`.
|
||||
Вычисление в `statements/checks.py` — чистая функция над `Mapping[str, Decimal | None]` по колонке: отсутствующий код —
|
||||
0; правая часть считается дважды — как записано и с членами `abs_terms` по модулю (1С показывает расходы со знаком минус,
|
||||
66н — в скобках, оба варианта допустимы); соотношение выполнено, если сходится хотя бы один вариант с допуском ±1 в
|
||||
единицах формы (`Decimal("1")`). Жёсткие (`strict`): ББ `1100 = 1110 + 1120 + 1130 + 1140 + 1150 + 1160 + 1170 + 1180 + 1190`
|
||||
(+1105 при наличии, `ASSUMPTION(OQ-37)`), `1200 = 1210 + 1220 + 1230 + 1240 + 1250 + 1260` (+1215), `1300 = 1310 + 1320 +
|
||||
1340 + 1350 + 1360 + 1370`, `1400 = 1410 + 1420 + 1430 + 1450`, `1500 = 1510 + 1520 + 1530 + 1540 + 1550`, `1600 = 1100 +
|
||||
1200`, `1700 = 1300 + 1400 + 1500`, `1600 = 1700`; ОФР `2100 = 2110 − 2120`, `2200 = 2100 − 2210 − 2220`, `2300 = 2200 +
|
||||
2310 + 2320 − 2330 + 2340 − 2350`. Мягкие (`strict: false`, только предупреждение): `2400 = 2300 − 2410 + 2420` и
|
||||
варианты ФСБУ 4/2023 с 2411/2412/2460 (`ASSUMPTION(OQ-12)`), `2500 = 2400 + 2510 + 2520 + 2530`, и «сумма пятизначных
|
||||
подстрок = родитель» (генерируется по коду: `1110 = 11101 + 11102 + …`, только когда подстроки есть). Проверка по каждой
|
||||
колонке отдельно; результат — `StatementChecks(relations_failed[{column_index, relation, expected, actual}],
|
||||
soft_relations_failed[…], duplicate_line_codes[])` + строки в предупреждения. Для форм без строк (нет колонки кодов,
|
||||
DISCLOSURE) `checks` не заполняется.
|
||||
|
||||
### D-8. Склейка листов-продолжений внутри книги, статус документа
|
||||
`statements/merge.py`: `parse_sheets` идёт по листам книги по порядку; блок `STATEMENT` открывает документ; лист
|
||||
`STATEMENT_CONTINUATION` присоединяется к последнему открытому документу формы, если число колонок-сумм в нём совпадает
|
||||
(или заголовка таблицы на листе нет и число колонок с числами совпадает) — строки добавляются в конец, `locator` =
|
||||
`sheet:<первый>` + `,<следующие имена>` через запятую. Продолжение без открытого документа (книга из одной страницы 2,
|
||||
лист ОСВ между страницами) → документ-одиночка: шапка пустая, тип по ОКУД маркера или `OTHER`, `needs_check`,
|
||||
предупреждение «страница-продолжение без шапки: загрузите форму одним файлом» (решение владельца, вопрос 2; переход к
|
||||
признаку `is_continuation` в контракте — две необязательных поля, без смены логики). Документ `ok` ⇔ все жёсткие
|
||||
соотношения сошлись по всем колонкам ∧ нет дубликатов кодов ∧ найдена колонка кодов ∧ есть хотя бы одна колонка-период ∧
|
||||
в шапке есть организация или ИНН ∧ документ не одиночка-продолжение. Шаблоны (коды есть, сумм нет) → все значения
|
||||
`null`, соотношения «0 = 0» сходятся, шапка пустая → `needs_check`.
|
||||
|
||||
### D-9. Контракт v1.2 — модели и копия
|
||||
`api/src/contract/result.py`: `StatementDocument.document_type: Literal["BALANCE","INCOME","DISCLOSURE","OTHER"]`;
|
||||
`StatementChecks` и `StatementDocument.checks: OptionalNotNull[StatementChecks] = None`; `FailedRelation(column_index,
|
||||
relation, expected: Money, actual: Money)`; `OsvHeader.period_start: date | None`, `period_end: date | None`,
|
||||
`is_from_year_start: bool | None`. `CONTRACT_VERSION = "1.2"`; копия схемы и примеры из `../docs/contracts` v1.2 (тест
|
||||
идентичности). Заполнение новых полей шапки ОСВ: `period_start`/`period_end` — по разобранному периоду (квартал/год →
|
||||
первое и последнее число; диапазон месяцев из issue #11 — его границы), `is_from_year_start` — `period_start` = 1 января.
|
||||
Если ветка #11 сливается раньше и уже вычисляет границы диапазона, заполнение делается здесь одной задачей на его
|
||||
результате; если позже — поля в этом change заполняются для квартала/года, а для диапазона — в #11. Поля необязательные,
|
||||
оба порядка совместимы.
|
||||
|
||||
### D-10. Словари форм — отдельные файлы
|
||||
`statement_headers.yaml` (заголовки форм и ОКУД по типам, маркер продолжения, метки шапки, синонимы «Код», «Наименование
|
||||
показателя», «Пояснения», признаки ОПФ, метки-исключения «аудитор»), `statement_periods.yaml` (месяцы, формы «на <дата>»,
|
||||
«за <N> месяцев <год>», «за <месяц> - <месяц> <год>», «за <год> г.», «<дд.мм.гггг>», «<год>»), `statement_checks.yaml`
|
||||
(D-7). `periods.yaml` ОСВ не переиспользуется и не правится — ветка #11 его меняет. Загрузчик `rules.py` расширяется
|
||||
моделями форм; тест покрытия словарей — на объединении файлов `statements-xlsx/**` и синтетики.
|
||||
|
||||
### D-11. Тесты, golden, синтетика
|
||||
Unit (`tests/unit/parser/statements/`) на `gridbuilder`; property (`tests/fixtures/test_statements_property.py`, маркер
|
||||
`fixtures`) по манифесту `tests/fixtures/statements_manifest.yaml` (каталог → организация, ИНН, ожидаемые типы и статусы,
|
||||
полная форма или страница); golden `../fixtures/golden/expected_statement_parse/<org>/<period>/<file>.json` через
|
||||
обобщённые `golden_helpers` и `make golden-update`; синтетика в `tests/synthetic/generate.py`: `st01_both_forms_one_sheet`,
|
||||
`st02_csv_like`, `st03_no_codes`, `st04_mixed_book` (ОСВ + ББ), `st05_continuation_only`; линтер «нет координат»
|
||||
включает `statements/**` и `book.py`; интеграционный тест воркера: книга НПЗ → `done`, `result_schema_version = "1.2"`,
|
||||
два документа. CLI печатает формы так же, как ОСВ.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Колонка кодов не найдена из-за кодов-чисел или объединений] → коды читаются как текст из любого типа ячейки, merged
|
||||
берётся по origin; синтетика `st02`. Остаточный риск закрывается фикстурой + синонимом, не `if`.
|
||||
- [Соотношения §7 не сходятся из-за знаков 1С] → двойной расчёт с `abs_terms`; при несходимости обоих — `needs_check`,
|
||||
перечень в `checks`; знак не угадывается.
|
||||
- [Постраничные xls всегда `needs_check`] → осознанно (вопрос 2); ядро в Срезе 3 решит склейку; данные не теряются.
|
||||
- [Конфликт с ветками #9–#11 в `osv.py`/`test_osv_parser.py`] → правило при rebase: логика ОСВ из их ветки, оркестрация
|
||||
книги отсюда; PR создаётся после rebase на main.
|
||||
- [Контракт 1.2 ломает идентичность копии в arm-core и scan-gateway] → bounded-PR с копией схемы и моделями раньше мержа
|
||||
парсера; порядок merge docs → arm-core/scan-gateway → tabular-parser → fixtures.
|
||||
- [Большие книги с формами] → те же лимиты и дедлайн, что у ОСВ; формы маленькие (≤ 100 строк на лист).
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Не требуется: сервис без состояния. После merge — пересборка образа; потребители контракта обновляют копии раньше.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Не блокируют: OQ-12 (знаки 2410/2420/2430 — мягкие проверки), OQ-36 (строки без кода — не отдаются), OQ-23
|
||||
(расшифровки — только классификация), OQ-37 (1105 как есть), OQ-131 (границы диапазона — D-9). Новые вопросы вносятся в
|
||||
`07-open-questions.md` сквозным change'ем: единицы «тыс. руб.» против значений с копейками в 1С-выгрузках; форма без
|
||||
колонки кодов (сопоставление по именам); страница-продолжение в отдельном файле (склейка в ядре, признак в контракте);
|
||||
строка без кода с суммами в ОФР ФСБУ (НПЗ «Реализация иного имущества…»); шаблон ФСБУ с «АКТИВ» и 1110 в одной строке.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| № | Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 2026-10-06 | D-2: продолжение определяется маркером «с.2» либо заголовком таблицы/колонкой кодов без заголовка формы | у НПЗ листы 2–5 без маркера, у 1С — с маркером | добавить признак в словарь |
|
||||
| 2 | 2026-10-06 | D-3: 0710003/0710004 и прочие ОКУД 0710xxx → DISCLOSURE, не OTHER | DBML различает расшифровку и прочее; ОДДС/ОИК для консолидации не нужны | переназначить тип |
|
||||
| 3 | 2026-10-06 | D-4: порог 60 % кодов в колонке и диапазоны 1100–1799 / 2100–2999 (+5xxx) | §12 п. 2; 1105 и 1215 внутри диапазона | подвинуть границы в словаре |
|
||||
| 4 | 2026-10-06 | D-4: колонка заголовка без периода не читается, одно предупреждение | §12 п. 4 «в extra», но у `statementDocument` поля `extra` нет; MINOR ради диагностики не оправдан | добавить `extra` в v1.x |
|
||||
| 5 | 2026-10-06 | D-5: ИНН аудитора исключается по метке «аудитор» выше | иначе у Унагранде второй ИНН в шапке | синоним в словаре |
|
||||
| 6 | 2026-10-06 | D-6: порог 10 % значений с дробной частью для предупреждения о единицах | единичные копейки в тысячах бывают от округления | число в словаре |
|
||||
| 7 | 2026-10-06 | D-7: допуск ±1 единица формы, отсутствующий код = 0, двойной расчёт с `abs_terms` | §7 «расхождение > 0», но формы в тыс. руб. округляются построчно; знаки 1С vs 66н | допуск в словаре |
|
||||
| 8 | 2026-10-06 | D-7: «сумма подстрок = родитель» — мягкая проверка, генерируется по коду | подстроки бывают неполными («прочее» без кода) | перевести в строгие |
|
||||
| 9 | 2026-10-06 | D-8: критерий `ok` документа включает «в шапке есть организация или ИНН» | без шапки ядро не сверит владельца; страница 2 должна быть `needs_check` | ослабить критерий |
|
||||
| 10 | 2026-10-06 | D-9: `period_start/period_end/is_from_year_start` в шапке ОСВ добавляются в контракт здесь, заполнение делится с #11 | владелец: поля в v1.2 (вопрос 5) | follow-up на заполнение |
|
||||
| 11 | 2026-10-06 | D-1: документы книги — сначала ОСВ, потом формы | детерминированный порядок для golden | порядок листов |
|
||||
74
openspec/changes/slice-2b-statement-parser/proposal.md
Normal file
74
openspec/changes/slice-2b-statement-parser/proposal.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Proposal: Срез 2b — парсер ББ и ОФР из Excel (§12), контракт v1.2
|
||||
|
||||
**Срез:** 2 «Загрузка файлов + табличный парсер», подсрез 2b (`../docs/context/09-delivery-plan.md`). **Репозиторий:**
|
||||
arm-tabular-parser. **Issue:** RSHB/arm-tabular-parser#14. **Сквозной change:** `../docs/openspec/changes/slice-2b-statement-parser`
|
||||
(контракт v1.2, §12, OQ; RSHB/arm-ka-docs#26). Golden и синтетика — отдельный PR в arm-ka-fixtures. Копии схемы v1.2 —
|
||||
bounded-PR в arm-core и arm-scan-gateway (без изменения поведения) до мержа этого change.
|
||||
|
||||
Решение владельца 06.10.2026 (brainstorming): объём среза — **только парсер и контракт**; маппинг `statementDocument` в
|
||||
ядро остаётся в Срезе 3.
|
||||
|
||||
## Why
|
||||
|
||||
После Среза 2a сервис разбирает только ОСВ, а листы с признаками ББ/ОФР пропускает с предупреждением
|
||||
`skipped: statement (slice 2b)`. Без форм из Excel нет пути «отчётность компании → консолидация»: ББ и ОФР дают строки
|
||||
КФО (§8), гудвилл (1310) и логические проверки (§7). В fixtures есть 15 реальных файлов пакета ДКБ в трёх раскладках
|
||||
(постраничные xls 1С, многолистовая книга ФСБУ 4/2023, выгрузка `TDSheet`) и четыре пустых шаблона 66н / ФСБУ 4/2023 —
|
||||
на них парсер обязан проходить без единого правила под файл, организацию или имя листа (§1.0, §12).
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Оркестратор книги** `api/src/parser/book.py` (`BookParser`): лимит размера, чтение книги, дедлайн, классификация
|
||||
листов (`detect.classify_sheet`), раздача листов конвейерам ОСВ и форм, сбор документов, предупреждений, метрик и
|
||||
статуса. `OsvParser` сужается до конвейера листа ОСВ (`parse_sheet`); `get_parser()` возвращает `BookParser`.
|
||||
- **Конвейер форм** `api/src/parser/statements/` (`StatementParser`): заголовок формы и ОКУД, колонка кодов,
|
||||
шапка документа, колонки-периоды, строки и значения, соотношения §7, склейка листов-продолжений внутри книги,
|
||||
расшифровки как `DISCLOSURE`.
|
||||
- **Словари** `api/rules/statement_headers.yaml`, `statement_periods.yaml`, `statement_checks.yaml` (соотношения §7 —
|
||||
данные, не код) с pydantic-валидацией при старте и тестом покрытия.
|
||||
- **Контракт v1.2** (копия `api/contracts/`, `CONTRACT_VERSION = "1.2"`): `statementDocument.document_type` +
|
||||
`DISCLOSURE`; `statementDocument.checks`; `osvDocument.header.period_start/period_end/is_from_year_start`.
|
||||
- **Тесты**: unit по модулям на синтетических сетках; property-тест на всех `../fixtures/statements-xlsx/**` по манифесту;
|
||||
golden `../fixtures/golden/expected_statement_parse/`; синтетика `../fixtures/statements-xlsx/synthetic/`; линтер «нет
|
||||
координат» на `statements/**` и `book.py`; покрытие словарей; интеграционный тест воркера с книгой НПЗ.
|
||||
- **Документы**: README (раздел «Парсер форм»), CLAUDE.md сервиса, `rules/README.md`, спеки `statement-parsing` (новая)
|
||||
и `tabular-recognition-service` (дельта).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `statement-parsing`: разбор ББ/ОФР из Excel по §12 — классификация листов и продолжений, тип формы, шапка, колонки-
|
||||
периоды, колонка кодов, строки и значения, соотношения §7, склейка листов, статусы и диагностика, словари.
|
||||
|
||||
### Modified Capabilities
|
||||
- `tabular-recognition-service`: «Парсер — заменяемый интерфейс» — реализация по умолчанию разбирает и ОСВ, и формы
|
||||
(`BookParser`), результат пишется с `schema_version = "1.2"`; каталог `api/rules/` содержит словари форм.
|
||||
|
||||
## Impact
|
||||
|
||||
- Код: `api/src/parser/book.py` (новое), `api/src/parser/statements/**` (новое), `api/src/parser/osv.py` (удаление
|
||||
`parse`/`_parse_book`), `api/src/parser/detect.py` (`classify_sheet`: продолжения форм, поиск признаков по области до
|
||||
таблицы), `api/src/parser/__init__.py` (`get_parser`), `api/src/contract/{common,result}.py` (v1.2), `api/contracts/`
|
||||
(копия), `api/rules/statement_*.yaml`, тесты, README, CLAUDE.md.
|
||||
- Таблицы DBML: не затрагиваются (сервис без БД). Поля результата — только из контракта v1.2.
|
||||
- Правила: §12 п. 1–8 (с решениями владельца 06.10.2026, внесёнными в §12 сквозным change'ем), §7 (формулы), §1.2 (числа).
|
||||
Допущения: `ASSUMPTION(OQ-12)` — знаки 2410/2420/2430 проверяются мягко; `ASSUMPTION(OQ-36)` — строки без кода не
|
||||
отдаются; `ASSUMPTION(OQ-23)` — расшифровки только классифицируются; `ASSUMPTION(OQ-37)` — код 1105 как пришёл.
|
||||
- Соседи: `../docs` (контракт v1.2, §12, OQ), `../fixtures` (golden, синтетика), `arm-core` и `scan-gateway` — копия
|
||||
схемы и pydantic-модели v1.2 отдельными bounded-PR, логика не меняется (ядро по-прежнему маппит только ОСВ).
|
||||
- Пересечение с параллельной работой по ОСВ (issues #9, #10, #11): модули `header.py`, `classify.py`, `checks.py`,
|
||||
`hierarchy.py`, `periods.yaml`, `osv_headers.yaml`, golden ОСВ в этом change не правятся; точка пересечения —
|
||||
`osv.py` и `test_osv_parser.py`, разрешается при rebase на main перед PR.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Маппинг `statementDocument` в `document` / `financial_statement_*` ядра, логические проверки §7 в ядре, подтверждение
|
||||
документа, чек-лист ББ/ОФР — Срез 3.
|
||||
- Склейка страниц одной формы из **разных файлов** (постраничные xls 1С): страница-продолжение без шапки отдаётся как
|
||||
документ с пустой шапкой и `needs_check`; признак продолжения в контракте — позже, при появлении потребителя в ядре.
|
||||
- Сопоставление строк по наименованиям при отсутствии колонки кодов (§12 п. 2, словарь `statement_lines.yaml`) —
|
||||
отложено до реального файла; форма без колонки кодов даёт `needs_check`.
|
||||
- Нормализация единиц измерения (деление на 1000, пересчёт рублей в тысячи) — ядро и аналитик; парсер отдаёт как есть.
|
||||
- Разбор содержимого расшифровок (`DISCLOSURE`), лист `К балансу`, рекомендуемые формы банка (OQ-23).
|
||||
- Изменения в парсере ОСВ, кроме переноса оркестрации книги в `BookParser`; заполнение новых полей шапки ОСВ
|
||||
`period_start/period_end/is_from_year_start` — в ветке issue #11 либо follow-up, см. design D-9.
|
||||
@ -0,0 +1,196 @@
|
||||
# statement-parsing Specification
|
||||
|
||||
## Purpose
|
||||
Разбор бухгалтерского баланса (0710001) и отчёта о финансовых результатах (0710002) из книг Excel в
|
||||
`statementDocument` контракта v1.2 по §12 `05-domain-rules.md`: тип формы, шапка, колонки-периоды, строки с кодами и
|
||||
значения, соотношения §7, склейка листов-продолжений внутри книги. Структура ищется по содержимому: ни буквы колонок,
|
||||
ни номера строк, ни имена листов не являются контрактом; синонимы и соотношения — данные в `rules/statement_*.yaml`.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Листы форм и их продолжения находятся по содержимому
|
||||
Классификация листа SHALL различать `OSV`, `STATEMENT`, `STATEMENT_CONTINUATION` и `OTHER`. Лист SHALL считаться
|
||||
`STATEMENT`, если в любой строке до первой строки заголовка таблицы формы или первой строки с кодом формы есть заголовок
|
||||
формы или код ОКУД из словаря `statement_headers.yaml`. Лист без заголовка формы SHALL считаться `STATEMENT_CONTINUATION`,
|
||||
если на нём есть маркер продолжения («Форма 0710001 с.2»), либо строка заголовка таблицы формы (ячейка «Код» и правее
|
||||
ячейки-периоды), либо колонка, в которой не меньше 60 % непустых значений — коды строк форм. Признак ОСВ SHALL
|
||||
проверяться первым. Тип формы SHALL определяться по заголовку или ОКУД: баланс / 0710001 → `BALANCE`, отчёт о
|
||||
финансовых результатах (о прибылях и убытках) / 0710002 → `INCOME`, пояснения, расшифровки и прочие формы 0710xxx →
|
||||
`DISCLOSURE`. Титул «Бухгалтерская отчетность» 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`; статус книги от него не зависит
|
||||
|
||||
### Requirement: Заголовок таблицы, колонка кодов и колонки-периоды определяются по тексту
|
||||
Строкой заголовка таблицы SHALL считаться первая после заголовка формы строка с ячейкой «Код»/«Коды» (синонимы в
|
||||
словаре) и хотя бы одной ячейкой-периодом правее; заголовок, разнесённый на две строки или с переносами внутри ячейки,
|
||||
SHALL склеиваться; строка нумерации колонок («1 2 3 4 5») SHALL пропускаться. Колонкой кодов SHALL считаться колонка,
|
||||
в которой не меньше 60 % непустых значений блока — четырёх- или пятизначные коды строк форм (1100–1799 для ББ, 2100–2999
|
||||
для ОФР и их пятизначные расширения, 5xxx для расшифровок); коды читаются как текст из ячеек любого типа. Колонкой
|
||||
наименований SHALL считаться ближайшая левее кодов колонка с преобладанием текста; колонка «Пояснения» в наименования
|
||||
MUST NOT попадать. Колонками-периодами SHALL быть все ячейки заголовка правее кодов с распознанным периодом, по порядку
|
||||
слева направо, `column_index` с нуля, `period_date` — дата или конец периода из `statement_periods.yaml`. Ячейки заголовка
|
||||
без периода MUST NOT становиться колонками; их значения не читаются, в предупреждения пишется одна строка. Если колонки
|
||||
кодов нет, документ SHALL отдаваться без строк со статусом `needs_check` и предупреждением «колонка кодов не найдена».
|
||||
|
||||
#### Scenario: Три раскладки, одни и те же колонки
|
||||
- **WHEN** разбираются `unagrande-co/2024-FY/Б.Б Лист 1.xls` (колонки «Пояснения | Наименование показателя | Код | На 31
|
||||
декабря 2024 г. | … 2023 | … 2022»), `npz/2025-Q3/…xlsx` («Пояснения | Наименование показателя | Код | На 30 сентября
|
||||
2025 г. | …») и `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` («Наименование показателя | Коды | За 9 месяцев 2025 года
|
||||
| На 31 декабря 2024 года | …», под шапкой строка «1 2 3 4 5»)
|
||||
- **THEN** у каждого документа три колонки с `period_date` `2024-12-31/2023-12-31/2022-12-31`, `2025-09-30/2024-12-31/2023-12-31`
|
||||
и `2025-09-30/2024-12-31/2023-12-31` соответственно, строка нумерации не стала строкой данных, «Пояснения» не попали в
|
||||
`line_name`
|
||||
|
||||
#### Scenario: Заголовок в две строки
|
||||
- **WHEN** разбирается шаблон `templates/ББ после 01.01.2025.xlsx` («На» в одной строке, «20__ г.» в следующей)
|
||||
- **THEN** колонки-периоды найдены по склеенному тексту, документ `BALANCE` без сумм, статус `needs_check`
|
||||
|
||||
#### Scenario: Колонка без периода
|
||||
- **WHEN** в синтетическом листе правее колонок дат есть колонка «Изменение, %»
|
||||
- **THEN** она не входит в `columns`, предупреждение «колонка без периода пропущена: Изменение, %»
|
||||
|
||||
#### Scenario: Нет колонки кодов
|
||||
- **WHEN** разбирается синтетический лист `st03_no_codes` (наименования и суммы без кодов)
|
||||
- **THEN** документ `BALANCE` с пустыми `lines`, статус `needs_check`, предупреждение «колонка кодов не найдена»
|
||||
|
||||
### Requirement: Шапка документа
|
||||
Поля шапки SHALL искаться по меткам из `statement_headers.yaml` в области от начала блока формы до строки заголовка
|
||||
таблицы; значение — ближайшая непустая ячейка правее метки, иначе первая непустая ячейка следующей строки. `inn` SHALL
|
||||
быть 10 или 12 цифр у метки «ИНН», причём строки после метки аудитора MUST NOT использоваться. `organization_name` SHALL
|
||||
браться после метки «Организация», а при пустом значении — из ближайшей строки выше с признаком ОПФ. `units` SHALL
|
||||
отдаваться текстом как в файле без нормализации. `report_date_text`/`report_date` SHALL браться из подзаголовка формы
|
||||
(«на <дата>», «за <период>»), иначе из тройки «число, месяц, год»; для ОФР `report_date` — конец периода. Найденные поля
|
||||
SHALL иметь `*_confidence = 100`. У документа без шапки все поля 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»
|
||||
|
||||
#### 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 давать `line` с `line_code` (текст без пробелов), `line_name` (trim,
|
||||
переносы заменены пробелом), `line_code_confidence = line_name_confidence = 100` и по одному `value` на каждую
|
||||
колонку-период. Строки без кода MUST NOT отдаваться (ASSUMPTION(OQ-36)); пятизначные подстроки SHALL отдаваться как
|
||||
обычные строки. Число SHALL приводиться к `Decimal` по §1.2 (скобки → минус, пробелы убираются); пусто, «–», «-» →
|
||||
`amount = null`, `is_recognized = true`; нечисловой текст → `amount = null`, `string_value` = текст, `is_recognized =
|
||||
false`; `confidence = 100`. Повтор кода внутри документа SHALL оставлять первую строку, записывать код в
|
||||
`checks.duplicate_line_codes` и переводить документ в `needs_check`. Единицы MUST NOT нормализоваться: при `units` с
|
||||
«тыс.» и дробной частью не меньше чем у 10 % значений в предупреждения SHALL писаться «значений с дробной частью при
|
||||
единице тыс. руб.: N».
|
||||
|
||||
#### Scenario: Подстроки и значения с копейками
|
||||
- **WHEN** разбирается `unagrande-co/2024-FY/Б.Б Лист 1.xls`
|
||||
- **THEN** есть строки `1110` («Нематериальные активы», `5971264.14`) и `11101` («Нематериальные активы в организации»,
|
||||
`5852464.14`); строка без кода с нулями после 11103 не отдана; предупреждение о дробной части при «тыс. рублей» есть
|
||||
|
||||
#### Scenario: Пустые значения и прочерки
|
||||
- **WHEN** в `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` у строки 1170 пусто в первой колонке и 10 в остальных
|
||||
- **THEN** `values[0].amount = null`, `values[1].amount = "10"`, все `is_recognized = true`
|
||||
|
||||
#### Scenario: Отрицательные в скобках
|
||||
- **WHEN** в синтетическом ОФР 2120 записана как «(6 162 067)»
|
||||
- **THEN** `amount = "-6162067"`
|
||||
|
||||
### Requirement: Соотношения §7 — данные, проверка по колонкам
|
||||
Соотношения SHALL храниться в `statement_checks.yaml` в виде `{form, relation, strict, abs_terms}` и вычисляться чистой
|
||||
функцией над «код → сумма» по каждой колонке-периоду отдельно и только по четырёхзначным кодам; отсутствующий код —
|
||||
0; соотношение SHALL считаться выполненным, если сходится правая часть как записана либо с членами `abs_terms` по
|
||||
модулю, с допуском ±1 в единицах формы. Нарушение жёсткого соотношения SHALL попадать в `checks.relations_failed`
|
||||
(`column_index`, `relation`, `expected`, `actual`) и переводить документ в `needs_check`; нарушение мягкого — в
|
||||
`checks.soft_relations_failed` и предупреждение. Проверка «сумма пятизначных подстрок = родитель» SHALL генерироваться по
|
||||
коду как мягкая, когда подстроки есть. Перечень нарушений SHALL дублироваться строками в `metrics.warnings`.
|
||||
|
||||
#### Scenario: Полные формы сходятся
|
||||
- **WHEN** разбираются книга НПЗ и обе формы Межениновской
|
||||
- **THEN** у документов `BALANCE` и `INCOME` `checks.relations_failed = []` по всем колонкам
|
||||
|
||||
#### Scenario: Знаки 1С
|
||||
- **WHEN** в ОФР Унагранде Компани (страница 1) расходы записаны со знаком минус (2120 = −6162067308.99)
|
||||
- **THEN** `2100 = 2110 − 2120` выполнено через вариант с `abs_terms`, в `relations_failed` его нет
|
||||
|
||||
#### Scenario: Испорченный итог
|
||||
- **WHEN** в синтетическом балансе 1600 на 5 больше суммы 1100 + 1200
|
||||
- **THEN** `relations_failed` содержит `{column_index: 0, relation: "1600 = 1100 + 1200", expected, actual}`, статус `needs_check`
|
||||
|
||||
#### Scenario: Мягкое соотношение
|
||||
- **WHEN** в ОФР подстроки 2411 + 2412 не равны 2410
|
||||
- **THEN** нарушение в `soft_relations_failed`, статус документа не меняется
|
||||
|
||||
### Requirement: Склейка листов-продолжений и статус документа
|
||||
Листы книги SHALL обрабатываться по порядку; лист `STATEMENT_CONTINUATION` SHALL присоединяться к последнему открытому
|
||||
документу формы при совпадении числа колонок-сумм, строки добавляются в конец, `locator` SHALL перечислять имена листов
|
||||
через запятую. Продолжение без открытого документа SHALL давать документ с пустой шапкой, типом по ОКУД маркера (иначе
|
||||
`OTHER`), статусом `needs_check` и предупреждением «страница-продолжение без шапки: загрузите форму одним файлом».
|
||||
Документ SHALL считаться `ok`, если все жёсткие соотношения сошлись по всем колонкам, дубликатов кодов нет, колонка
|
||||
кодов найдена, есть хотя бы одна колонка-период, в шапке есть организация или ИНН и документ не является одиночным
|
||||
продолжением. Книга SHALL получать `done`, если все документы ОСВ и форм `ok`, иначе `needs_check`; книга без документов
|
||||
ОСВ и форм SHALL давать `error INVALID_DOCUMENT`.
|
||||
|
||||
#### Scenario: Баланс на пяти листах
|
||||
- **WHEN** разбирается книга НПЗ
|
||||
- **THEN** два документа: `BALANCE` с `locator = "sheet:Бухгалтерский баланс. Лист 1,Бухгалтерский баланс. Лист 2,…,Лист 5"`
|
||||
и строками от 1105 до 1700 включительно, `INCOME` с тремя листами; статус книги `done`
|
||||
|
||||
#### 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` выполнено, `1700`/`1300` отсутствуют
|
||||
(считаются нулём: соотношение `1600 = 1700` нарушено → `needs_check` с перечнем)
|
||||
|
||||
#### Scenario: Смесь ОСВ и форм в одной книге
|
||||
- **WHEN** разбирается синтетическая книга `st04_mixed_book` (лист ОСВ 60 и лист баланса)
|
||||
- **THEN** результат содержит `osvDocument` и `statementDocument`, статус книги по обоим
|
||||
|
||||
### Requirement: Словари форм, координаты, golden
|
||||
Словари `statement_headers.yaml`, `statement_periods.yaml`, `statement_checks.yaml` SHALL валидироваться при старте;
|
||||
каждая запись SHALL срабатывать хотя бы на одном файле `../fixtures/statements-xlsx/**` или синтетике. Код
|
||||
`src/parser/statements/**` и `src/parser/book.py` MUST NOT содержать литералов координат (линтер `test_no_coordinates`).
|
||||
Снимки `../fixtures/golden/expected_statement_parse/**` SHALL покрывать все файлы `statements-xlsx/**` и обновляться
|
||||
только `make golden-update`; property-тест по манифесту `tests/fixtures/statements_manifest.yaml` SHALL проверять тип,
|
||||
ИНН, ожидаемый статус и сходимость жёстких соотношений для полных форм.
|
||||
|
||||
#### Scenario: Новый синоним без релиза кода
|
||||
- **WHEN** в `statement_headers.yaml` добавлен синоним заголовка «Баланс предприятия» и файл с таким заголовком
|
||||
- **THEN** лист классифицирован как `BALANCE` без изменения кода
|
||||
|
||||
#### Scenario: Мёртвая запись словаря
|
||||
- **WHEN** запись словаря не срабатывает ни на одном файле
|
||||
- **THEN** тест покрытия падает с именем записи
|
||||
|
||||
#### Scenario: Регрессия по снимкам
|
||||
- **WHEN** результат разбора любого файла `statements-xlsx/**` отличается от снимка
|
||||
- **THEN** golden-тест падает с diff; обновление — только `make golden-update` и отдельный коммит в fixtures
|
||||
@ -0,0 +1,32 @@
|
||||
# Spec Delta: tabular-recognition-service
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Парсер — заменяемый интерфейс
|
||||
Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о
|
||||
NATS и S3; воркер MUST NOT зависеть от конкретной реализации. Реализация по умолчанию (`get_parser()`) SHALL быть
|
||||
оркестратором книги `BookParser`: он читает книгу один раз, классифицирует листы по содержимому и отдаёт листы ОСВ
|
||||
конвейеру спеки `osv-parsing`, листы форм и их продолжения — конвейеру спеки `statement-parsing`; прочие листы
|
||||
пропускаются с предупреждением. Файл результата SHALL писаться с `schema_version = "1.2"`; документы книги — ОСВ в
|
||||
порядке листов, затем формы в порядке листов. Заглушка `NullParser` (`error NOT_IMPLEMENTED`) SHALL использоваться только
|
||||
в тестах воркера и MUST NOT быть реализацией по умолчанию. Реализация по умолчанию SHALL создаваться один раз при старте
|
||||
приложения (словари читаются и проверяются там же). Каталог `api/rules/` SHALL содержать словари ОСВ (`osv_headers`,
|
||||
`osv_groups`, `periods`, `data_types`), словари форм (`statement_headers`, `statement_periods`, `statement_checks`) и README.
|
||||
|
||||
#### Scenario: Подмена реализации в тесте
|
||||
- **WHEN** в тесте воркеру передан парсер, возвращающий один `osvDocument`
|
||||
- **THEN** файл результата с этим документом записан в S3, конверт имеет `status = done` и `result_s3` на этот файл
|
||||
|
||||
#### Scenario: Реальная ОСВ через воркер
|
||||
- **WHEN** в интеграционном тесте в S3 лежит `rb/2024-FY/ОСВ_66 за 2024.xls` и опубликован запрос `file_type = XLS`
|
||||
- **THEN** после ack публикуется конверт `done` с `result_schema_version = "1.2"`, файл результата проходит схему v1.2 и
|
||||
содержит один `osvDocument` с `account_code_raw = "66"`
|
||||
|
||||
#### Scenario: Реальные формы через воркер
|
||||
- **WHEN** в интеграционном тесте в S3 лежит книга `statements-xlsx/npz/2025-Q3/…xlsx` и опубликован запрос `file_type = XLSX`
|
||||
- **THEN** после ack публикуется конверт `done` с `result_schema_version = "1.2"`, файл результата содержит два
|
||||
`statementDocument` (`BALANCE`, `INCOME`) с `checks.relations_failed = []`
|
||||
|
||||
#### Scenario: Книга без ОСВ и форм
|
||||
- **WHEN** в S3 лежит книга Excel, ни один лист которой не имеет признаков ОСВ или формы
|
||||
- **THEN** публикуется конверт `error`, `error.code = INVALID_DOCUMENT`, сообщение называет число просмотренных листов
|
||||
52
openspec/changes/slice-2b-statement-parser/tasks.md
Normal file
52
openspec/changes/slice-2b-statement-parser/tasks.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Tasks: Срез 2b — парсер ББ и ОФР из Excel (tabular-parser)
|
||||
|
||||
Каждая задача ≤ половины дня, начинается с тестов, заканчивается зелёным `make check`. Пути — от корня репозитория.
|
||||
Фикстуры — `../fixtures` (worktree `.wt/slice-2b/fixtures`, отдельный PR в arm-ka-fixtures); контракт v1.2 —
|
||||
`../docs/contracts` (ветка `feat/26-slice-2b-statement-parser`). Детализация шагов и модели исполнителей — в плане
|
||||
superpowers (`docs/superpowers/plans/`), который пишется после согласования этой спеки.
|
||||
|
||||
## 1. Основание
|
||||
|
||||
- [ ] 1.1 Контракт v1.2: копия `../docs/contracts/recognition-result.schema.json` и примеров в `api/contracts/`;
|
||||
`StatementDocument.document_type` + `DISCLOSURE`, `StatementChecks`/`FailedRelation`, `OsvHeader.period_start/
|
||||
period_end/is_from_year_start`; `CONTRACT_VERSION = "1.2"`; тесты контракта и воркера (`result_schema_version`).
|
||||
- [ ] 1.2 Фикстуры форм в тестах: `conftest.py` — параметризация `statement_file` по `../fixtures/statements-xlsx/**`
|
||||
(кроме `synthetic`), `statement_synthetic_file`; манифест `tests/fixtures/statements_manifest.yaml`; обобщение
|
||||
`golden_helpers.py` на каталог `expected_statement_parse/`; `make golden-update`/`golden-check` для обоих каталогов.
|
||||
- [ ] 1.3 Словари форм: `api/rules/statement_headers.yaml`, `statement_periods.yaml`, `statement_checks.yaml`; модели в
|
||||
`rules.py`; тест на битый словарь; `rules/README.md`.
|
||||
|
||||
## 2. Оркестратор книги
|
||||
|
||||
- [ ] 2.1 `detect.classify_sheet`: `SheetKind.STATEMENT_CONTINUATION`, признаки формы по области до таблицы (D-2); unit
|
||||
на `gridbuilder`, fixtures-тест классификации всех листов `statements-xlsx/**` и `osv/**` (ОСВ не изменились).
|
||||
- [ ] 2.2 `book.py` (`BookParser`, D-1): перенос лимита/чтения/дедлайна/цикла из `OsvParser`, `OsvParser` без `parse`;
|
||||
`get_parser` собирает `BookParser`; `test_osv_parser.py` → `test_book_parser.py` + unit на смесь листов; golden ОСВ и
|
||||
property ОСВ без изменений.
|
||||
|
||||
## 3. Конвейер форм
|
||||
|
||||
- [ ] 3.1 `statements/detect.py`: `find_forms` — заголовок формы, ОКУД, тип, разрез листа на блоки, расшифровки (D-3).
|
||||
- [ ] 3.2 `statements/columns.py`: заголовок таблицы, склейка двухстрочного заголовка, строка нумерации, колонка кодов,
|
||||
колонка наименований, колонки-периоды и даты (D-4); unit на три раскладки и шаблоны.
|
||||
- [ ] 3.3 `statements/header.py`: шапка по меткам, ИНН не аудитора, организация над меткой, дата из тройки (D-5).
|
||||
- [ ] 3.4 `statements/lines.py`: строки с кодом, значения, прочерки, скобки, дубликаты, предупреждение о единицах (D-6).
|
||||
- [ ] 3.5 `statements/checks.py`: соотношения из словаря, `abs_terms`, допуск, мягкие, подстроки, `StatementChecks` (D-7).
|
||||
- [ ] 3.6 `statements/merge.py` + `statement.py` (`StatementParser.parse_sheets`): склейка продолжений, одиночная
|
||||
страница, `locator`, критерий `ok` (D-8); unit на книгу из нескольких листов.
|
||||
|
||||
## 4. Реальные файлы, синтетика, регрессия
|
||||
|
||||
- [ ] 4.1 Синтетика `tests/synthetic/generate.py`: `st01_both_forms_one_sheet`, `st02_csv_like`, `st03_no_codes`,
|
||||
`st04_mixed_book`, `st05_continuation_only` → `../fixtures/statements-xlsx/synthetic/`.
|
||||
- [ ] 4.2 Property-тест `tests/fixtures/test_statements_property.py` по манифесту: типы, ИНН, статусы, сходимость §7 для
|
||||
полных форм, шаблоны не падают; golden для всех файлов `statements-xlsx/**`.
|
||||
- [ ] 4.3 Покрытие словарей форм в `test_rules_coverage.py`; линтер координат на `statements/**` и `book.py`.
|
||||
- [ ] 4.4 Заполнение `period_start/period_end/is_from_year_start` в шапке ОСВ для квартала/года (D-9); для диапазона
|
||||
месяцев — по результату ветки #11 (одна задача здесь либо follow-up).
|
||||
- [ ] 4.5 Интеграционный тест воркера: книга НПЗ → `done`, два документа, `result_schema_version = "1.2"`; CLI печатает формы.
|
||||
|
||||
## 5. Завершение
|
||||
|
||||
- [ ] 5.1 README (раздел «Парсер форм»: конвейер, словари, статусы, как запустить на файле), CLAUDE.md сервиса,
|
||||
`rules/README.md`; `make check-full`; rebase на main (ветки #9–#11), PR с «Rulings I made» и находками.
|
||||
Loading…
x
Reference in New Issue
Block a user