From 6d39800e025ea32b5a04f23cd1e4bdb40484ecfb Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 21:17:12 +0400 Subject: [PATCH] =?UTF-8?q?docs(2b):=20=C2=A712=20=D0=91=D0=91/=D0=9E?= =?UTF-8?q?=D0=A4=D0=A0=20=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=B8,=20=C2=A77=20=D0=B6=D1=91=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B8=D0=B5/=D0=BC=D1=8F=D0=B3=D0=BA=D0=B8=D0=B5,?= =?UTF-8?q?=20=C2=A71.1/=C2=A71.3=E2=80=93=C2=A71.5=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=84=D0=B0=D0=BA=D1=82=D1=83=20PR=20tabular-parser=20#13/#15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #26 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b --- context/05-domain-rules.md | 129 ++++++++++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 29 deletions(-) diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 03c3618..e965325 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -86,6 +86,13 @@ r11 Договоры ← "period_text":"1 квартал 2025 г.","data_type":"BU","groups":[{"type":"ACCOUNT"},{"type":"COUNTERPARTY","has_inn":true}, {"type":"CONTRACT"}]}` и → `document(document_type=OSV, osv_account_code, period_year, period_quarter, is_cumulative, locator)`. +**Шапка без меток и без строки организации (реализовано, PR tabular-parser #13, OQ-134; ОСВ 63 Новоросметалл).** Период +может стоять отдельной ячейкой («Бухгалтерский | 9 месяцев 2025 г.»: `periods.yaml` — `standalone`, `data_types.yaml` — +`standalone_header`), организация — хвостом названия ведомости, а колонки счёта — подписями «Счет: Код / Наименование» +(`osv_headers.yaml` — `column_captions`: «Код» и «Наименование» под группой «Счет» не группировки). Код счёта берётся из +первой строки-счёта таблицы, если «по счету …» в шапке нет, — с предупреждением «код счёта … из строки таблицы». Текст +строки может стоять правее колонки текста: уровень задаёт колонка, в которой он найден. + ### 1.2 Колонки сумм Заголовки сумм объединены (merged) и позиции колонок плавают между файлами даже одной организации. Определять по тексту @@ -99,8 +106,13 @@ r11 Договоры ← Для каждой строки табличной части: `row_number` (исходный номер), `level` (outline level Excel, в xls — `rowinfo_map`; без группировок — каскад §1.0 п. 5), `row_value` (текст первой колонки), шесть сумм, ИНН (**только из колонки ИНН**; в ячейке оставляются цифры, результат только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; число из 9/11 цифр — ИНН региона 01–09 -с потерянным ведущим нулём — при извлечении из выбранной колонки дополняется нулём слева, R-24; ИНН внутри текста строки контрагента -(«Полиграф, 3528315710») парсер не извлекает: `counterparty_name` = весь текст строки, **[открыто]** OQ-100), +с потерянным ведущим нулём — при извлечении из выбранной колонки дополняется нулём слева, R-24; ИНН контрагента — в порядке +приоритета: колонка ИНН (в том числе склеенная «ИНН/КПП»), затем строка без сумм **прямо над** названием на том же уровне +оформления (в `rows` не попадает, предупреждение «ИНН … над строкой … отнесён к ней»), затем конец текста названия +(«Полиграф, 3528315710», «ООО Альфа (ИНН 7701234567)» — 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»). +Из текста ИНН отрезается всегда: `counterparty_name` без ИНН и разделителя, `row_value` — исходный текст; только для строк +`COUNTERPARTY`, договоры и прочее не трогаются; правила — `osv_headers.yaml` (`counterparty_inn_suffix`, `inn_only_row`, +`inn_kpp_cell`); **реализовано**, OQ-100, OQ-130 (tabular-parser #9, PR #15; синтетика `s17`–`s19`), `data_type` (строки «USD», «Вал.», «Кол.» под контрагентом → CURRENCY/QUANTITY; по умолчанию — из шапки). Строки «Вал.», «Кол.» и трёхбуквенный код валюты («USD») — не уровень группировки, а вид сумм родителя: @@ -159,7 +171,12 @@ r11 Договоры ← значения родителя и выше не затронуты); - родитель (0, 0) при детях (X Дт, X Кт) принимается (нетто 0) — осознанное поведение, закреплено тестом; - вне R-21: текст вида «12.ООО» принимается за счёт (§1.3, R-16). -- Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (**пустая ОСВ**: заголовок и +- Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0); если корней уровня 0 несколько (лист без + строки корневого счёта: верхний уровень — субсчета 58.01, 58.03, OQ-133, PR #13) — «Итого» == **сумме строк уровня 0** + (показатели без суммы и сама «Итого» не входят; сальдо — по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя + выше, R-21); при одном корне сверка прежняя. Вторая итоговая строка сразу за «Итого» («Итого развернутое») — тоже `TOTAL`, + в сверку не входит; строка сумм без подписи в конце таблицы принимается за «Итого» с предупреждением. + Если корня нет (**пустая ОСВ**: заголовок и сразу «Итого» — в фикстурах две, schastl 58 и 66 за 2 кв. 2025) — «Итого» должна быть пустой или нулевой; такая ОСВ штатна: `done`, `hierarchy=trivial`, `metrics.rows = 0`, предупреждение «нет строк данных». Если строка «Итого» не найдена вовсе — сверка невозможна: `total_row_matches_root = false` и предупреждение «строка «Итого» не найдена» @@ -179,6 +196,15 @@ r11 Договоры ← | 4 квартал, IV кв. | Q4 | **false** [открыто] | | год, за 2025 год, 12 месяцев, годовая | Q4 | true | +**Период-диапазон месяцев** («Январь - Май 2025 г.», «Октябрь 2024 г. – Сентябрь 2025 г.»; `periods.yaml`: `months`, `range`; +OQ-131, PR #13 и 2b): парсер заполняет границы `period_start`/`period_end`/`is_from_year_start` (контракт v1.2; для квартала +и года: конец — последний день квартала, начало — 1 января у нарастающего и первый день квартала у квартального; +`is_from_year_start` — `period_start` = 1 января). Диапазон «январь – конец квартала» — нарастающий +(`is_cumulative = true`, квартал по месяцу конца), ровно один квартал — квартальный, остальное (скользящие 12 месяцев, +полгода с апреля, конец не на границе квартала) — `period_year` = год конца, `period_quarter = null`, +`is_cumulative = null`, границы по тексту и предупреждение «не с начала года» / «конец диапазона не совпадает с концом +квартала»: ядро такой документ к версии не привязывает (OQ-131). + `ASSUMPTION(OQ-02)`: парсер определяет `period_quarter` и `is_cumulative` **только по тексту периода** (`rules/periods.yaml`, правила идут от специфичных к общим): квартальная ведомость Q2–Q4 считается не нарастающей; сверку по данным (`Q3.opening == Q1.opening`) выполняет ядро и может переопределить признак. `period_year` — четыре @@ -420,6 +446,14 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou Межформенная: `1370(отчётная дата) − 1370(начало года) = 2400(ОФР за период)` — при расхождении КА может акцептовать с комментарием (`financial_statement_meta.checks_accepted_*`). +**Парсер tabular-parser (§12 п. 7)** считает **жёсткими** (нарушение → `needs_check`) соотношения ББ — `1100`, `1200` +(+1215), `1300`, `1400`, `1500`, `1600 = 1100 + 1200`, `1700 = 1300 + 1400 + 1500`, `1600 = 1700` — и ОФР — `2100`, `2200`, +`2300`; **мягкими** (предупреждение) — `2400 = 2300 − 2410 + 2420` и варианты ФСБУ 4/2023 (`2400 = 2300 + 2410 + 2460`, +`2410 = 2411 + 2412`), `2500 = 2400 + 2510 + 2520 + 2530` и «сумма пятизначных подстрок = родитель» +(`ASSUMPTION(OQ-12)`). Допуск ±1 в единицах формы, расходы — двойной расчёт знаков (`abs_terms`). Межформенная проверка +`1370 − 1370(начало года) = 2400` парсером не выполняется. Это сигнал о структуре и данных файла, а не замена проверок +ядра: ядро (Срез 3) пересчитывает §7 и ведёт акцепт (`financial_statement_meta.checks_accepted_*`). + Когда: после распознавания; после «Сохранить» правок; при «Подтвердить данные». Непройдено → `document.status = NEEDS_CHECK`; пройдено/акцептовано + нажато «Подтвердить» → `CONFIRMED` + `audit_event(DOCUMENT_CONFIRM)`. Отрицательные значения в формах — в скобках `(2100)` → `-2100`. Значения с `confidence < 100` подсвечиваются. @@ -742,30 +776,67 @@ OQ-92 (пустые «Организация»/«Группа компаний» что и у `scan-gateway`, чтобы ядро маппило ББ/ОФР одинаково независимо от источника (`confidence = 100`, `is_recognized = true` для табличного пути). -1. **Классификация листа**: ячейки с «Бухгалтерский баланс» / «0710001» → BALANCE; «Отчет о финансовых результатах» / - «Отчет о прибылях и убытках» / «0710002» → INCOME. На одном листе могут быть обе формы подряд (ББ, затем ОФР) — - разрезать по второму заголовку; в одном файле несколько листов — несколько `document`. Листы ОСВ и форм в одном - файле — допустимо. -2. **Колонка кодов строк**: та, где ≥ 60 % непустых значений — четырёхзначные коды из допустимого набора - (1110–1700 для ББ, 2110–2500 для ОФР, плюс 5xxx для расшифровок). Заголовок «Код» необязателен. Если колонки кодов - нет — попытаться сопоставить по наименованиям показателей (словарь `rules/statement_lines.yaml`: «Основные средства» → - 1150) с пометкой `line_code_confidence < 100` и warning; агент-аналитик подтвердит. -3. **Колонка наименований**: ближайшая текстовая слева от кодов; «Пояснения» (1–2 символа/цифры) слева от неё — не - наименование. -4. **Колонки-периоды**: ячейки заголовка справа от кодов, матчащие дату («На 31 декабря 2024 г.», «31.12.2024», - «2024», «За 9 месяцев 2025 г.», «За январь–сентябрь 2025 г.») → `financial_statement_column` с `period_date` - (для ОФР — дата конца периода) и `is_current` = самая поздняя. Число колонок — от 1 до 3+. Колонки без даты - («Изменение», «%») — в `extra`. -5. **Значения**: Decimal по правилам §1.2; скобки → минус; «–»/«-»/пусто → NULL. Единица измерения — из шапки - («тыс. руб.», «млн руб.», ОКЕИ 384/385) → `header.units`; если не найдена — по порядку величины относительно - ОСВ той же организации **не угадывать**, оставить NULL (ядро спросит). -6. **Шапка**: организация, ИНН, дата, ОКПО/ОКОПФ/ОКФС/ОКВЭД — регулярками по области выше таблицы; как в §1.0 п. 6. -7. **Самопроверка**: формулы §7 по каждой колонке; расхождение > 0 (в единицах формы) → `needs_check` с перечнем - непройденных соотношений в `metrics.warnings`. Это не замена проверок ядра, а сигнал, что структура угадана неверно - (например, перепутаны колонки периодов). -8. Расшифровки к строкам (лист `К балансу`, рекомендуемые формы банка) — в первой очереди **не разбирать**; классифицировать - как `DISCLOSURE`/`OTHER` и сохранить `locator`. +**Статус: реализовано (Срез 2b, tabular-parser feat/14, контракт v1.2; решения владельца 06.10.2026).** Модули — +`api/src/parser/statements/`, словари — `api/rules/statement_headers.yaml`, `statement_periods.yaml`, +`statement_checks.yaml`; ожидания по реальным файлам — `api/tests/statements_manifest.yaml` (06-test-data §4б). -Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_*` для PDF и Excel; экран -«Проверка распознавания» для Excel-источника показывает слева не страницы, а рендер листа (или просто таблицу) — -UI-решение вне парсера. +1. **Классификация листов**: лист ОСВ → форма → продолжение → прочее. Форма: заголовок («Бухгалтерский баланс», + «Отчет о финансовых результатах», «Отчет о прибылях и убытках») или ОКУД в **любой** строке до таблицы (у НПЗ он стоит + под титулом); титул «Бухгалтерская отчетность» — не тип, тип берёт ближайший следующий заголовок. Тип по ОКУД: + 0710001 → `BALANCE`, 0710002 → `INCOME`, прочие 0710xxx (0710003 капитал, 0710004 ДДС) и пояснения («Пояснения к + бухгалтерскому балансу», «Расшифровка») → `DISCLOSURE`; при противоречии ОКУД и заголовка — тип по ОКУД и + предупреждение. Две формы на одном листе — по второму заголовку, в книге несколько листов — несколько `document`. + Продолжение: лист без заголовка формы, но с маркером «Форма … с.2», либо с подписью «Код» и периодами правее, либо с + колонкой кодов. ОСВ и формы в одной книге допустимы. +2. **Колонка кодов**: в блоке ≥ 60 % непустых значений — коды (ББ 1100–1799, ОФР 2100–2999, расшифровки 5xxx, + пятизначные — по первым четырём цифрам), кодов не меньше `min_codes` = 3; заголовок «Код/Коды» необязателен, числа вида + `1110.0` читаются как текст «1110». Колонки нет — документ без строк, `needs_check`, предупреждение «колонка кодов не + найдена»; сопоставление по наименованиям строк **не делается** (решение владельца, OQ-138). +3. **Колонка наименований** — ближайшая левее кодов с преобладанием текста; «Пояснения» (пустые или ≤ 3 символов) не + наименование. +4. **Колонки-периоды**: ячейки заголовка правее кодов с распознанным периодом («На 31 декабря 2024 г.», «31.12.2024», + «За 9 месяцев 2025 г.», «За январь - сентябрь 2025 г.», «2024»), по порядку слева направо (`column_index` 0, 1, 2…); + `period_date` — дата или конец периода; `is_current` в контракте нет — ядро берёт самую позднюю. Заголовок в две строки + («На» + «20__ г.», перенос внутри ячейки) склеивается; строка нумерации «1 2 3 4 5» пропускается. Колонки без периода + («Изменение», «%») **не читаются** (одно предупреждение на документ): у `statementDocument` поля `extra` нет. +5. **Значения**: `Decimal`, скобки → минус, «–»/пусто → `null`, нечисловой текст → `string_value`, + `is_recognized = false`. **Единицы не нормализуются**: `amount` как в ячейке, `header.units` — текст шапки; при «тыс.» и + ≥ 10 % значений с дробной частью — предупреждение «N значений с дробной частью при единице тыс. руб.» (OQ-137, + решение владельца). Строки без кода (разделы, «в том числе:», «Реализация иного имущества…» у НПЗ) не отдаются + (`ASSUMPTION(OQ-36)`, OQ-140); повтор кода — остаётся первая строка, код в `checks.duplicate_line_codes`, `needs_check`. + Пятизначные подстроки (11101, 12301…) отдаются как обычные строки. +6. **Шапка**: организация (после метки «Организация», иначе текст с признаком ОПФ строкой выше метки — НПЗ), ИНН 10/12 + цифр у метки «ИНН» (**ИНН аудитора не берётся**: строки после метки «аудитор» исключены), ОКПО, ОКОПФ/ОКФС, ОКЕИ, адрес, + единицы, `report_date_text`/`report_date` — из подзаголовка формы или тройки «число | месяц | год» у метки «Дата» (для + ОФР — конец периода). Шапка другого листа не используется: у ОФР НПЗ нет метки «ИНН» → `inn = null`. Найденное поле — + `*_confidence = 100`, не найденное — `null`. +7. **Самопроверка**: соотношения §7 по каждой колонке — данные `statement_checks.yaml`. **Жёсткие** (нарушение → + `needs_check`): ББ `1100`, `1200` (+1215), `1300`, `1400`, `1500`, `1600 = 1100 + 1200`, `1700 = 1300 + 1400 + 1500`, + `1600 = 1700`; ОФР `2100`, `2200`, `2300`. **Мягкие** (только предупреждение, `ASSUMPTION(OQ-12)`): `2400 = 2300 − + 2410 + 2420` и варианты ФСБУ 4/2023, `2500`, «сумма пятизначных подстрок = родитель». Допуск ±1 **в единицах формы**, + отсутствующий код = 0, соотношение проверяется, только если присутствует его цель; правая часть считается дважды — как + записано и с членами `abs_terms` по модулю (1С выгружает расходы со знаком минус, 66н — в скобках). Результат — + `documents[].checks` (`relations_failed`, `soft_relations_failed`, `duplicate_line_codes`) и предупреждения. + **Фактически сходятся полные формы только у Межениновской**: книга НПЗ (`npz/2025-Q3`) — ББ `needs_check`: `1600 = 1700` + расходится на 1.94 / 9.02 / 2.48 руб., потому что значения в рублях с копейками при шапке «тыс. руб.»; допуск ±1 остаётся + (решение владельца 06.10.2026, вариант А, OQ-137). ОФР НПЗ и ОФР Унагранде (стр. 1) тоже `needs_check` по 2100/2200/2300 + по той же причине; ББ Унагранде (стр. 1) — только актив, `1600 = 1700` не сходится. Это сигнал о данных файла, а не о + неверно угаданной структуре; допуск под файл не подгоняется. +8. **Расшифровки** (0710003/0710004, пояснения, лист «Примечания» шаблона) — `DISCLOSURE` с `locator` и шапкой, если + нашлась, без `columns`/`lines`; на статус не влияет (OQ-23). Книга только из таких листов — `done`. +9. **Склейка**: листы-продолжения внутри книги присоединяются к последнему открытому документу формы, если число колонок + сумм совпадает; строки добавляются в конец, `locator` перечисляет листы через запятую. **Страницы в разных файлах не + склеиваются** (1С печатает форму постранично, один файл = одна страница): страница 2 без первой — документ с пустой шапкой + и `needs_check`, предупреждение «страница-продолжение без шапки: загрузите форму одним файлом» (решение владельца, OQ-139). + Продолжение без колонки кодов при непустом результате не теряется молча — `needs_check`. +10. **Статус**: документ `ok` ⇔ жёсткие соотношения сошлись по всем колонкам ∧ нет дубликатов кодов ∧ найдена колонка кодов + ∧ есть колонка-период ∧ в шапке есть организация или ИНН ∧ документ не одинокое продолжение. Книга — `done`, если `ok` + у всех документов, иначе `needs_check`; книга без листов ОСВ и форм — `INVALID_DOCUMENT`. Порядок документов: сначала + ОСВ в порядке листов, затем формы. Шаблоны (коды есть, сумм нет, шапки нет) — `needs_check`. +11. **Отложено**: запасной путь по именам строк (OQ-138), нормализация единиц (OQ-137), признак продолжения в контракте и + склейка страниц из разных файлов в ядре (OQ-139, Срез 3), строки без кода (OQ-140), образцы ФСБУ (OQ-141), лист + продолжения с единственной кодовой строкой (OQ-142: лист ОФР НПЗ с одной строкой 2400 не распознаётся как форма и + теряется, `min_codes` = 3). + +Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_*` для PDF и Excel (маппинг форм — +Срез 3); экран «Проверка распознавания» для Excel-источника показывает не страницы, а таблицу — UI-решение вне парсера.