24 KiB
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 у
Celltext = "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