Andrey Epifancev d6d795787b openspec: сценарий двухстрочного заголовка — на синтетике st02, шаблоны — отдельный сценарий
Шаблоны ФСБУ разносят год по ячейкам «20» и « г.», по тексту дату не собрать; требование к ним — разбор без ошибки
и needs_check.

Refs #14

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 13:54:00 +04:00

22 KiB
Raw Blame History

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 разбирается синтетический лист st02_csv_like («На 31 декабря» в одной строке, «2024 г.» в следующей, коды без подписи «Код»)
  • THEN колонки-периоды найдены по склеенному тексту (2024-12-31, 2023-12-31), колонка кодов найдена без подписи

Scenario: Пустые шаблоны форм

  • WHEN разбираются templates/*.xlsx (образцы 66н и ФСБУ 4/2023: коды есть, сумм и шапки организации нет; год в заголовке колонки разнесён по ячейкам «20» и « г.»)
  • THEN документы BALANCE/INCOME со строками-кодами без значений, статус 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