arm-ka-docs/context/05-domain-rules.md
2026-10-02 23:02:44 +04:00

418 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 05. Правила предметной области (выжимка из ТЗ, имена — v5.13)
Каждый раздел: источник в ТЗ → правило → где в модели. Где ТЗ молчит или противоречит — пометка **[открыто]** и
ссылка на `07-open-questions.md`.
---
## §1. Парсинг ОСВ (tabular-parser + маппинг в ядре)
Источник: «Парсинг ОСВ» (DOC-260010), FNR01 (DOC-037740), реальные файлы `fixtures/osv/`.
### 1.0 Главный принцип: структура определяется динамически, не по координатам
ОСВ приходят из разных конфигураций 1С (8.2/8.3, УПП, ERP, БП 3.0), из других учётных систем и после ручной правки
бухгалтером. **Ни одна буква колонки, ни один номер строки, ни одно имя листа не являются контрактом.** Фикстуры в
`fixtures/osv/` — это примеры двух организаций, а не спецификация формата. Парсер обязан:
1. **Найти лист(ы) с ОСВ** по содержимому: любая ячейка, матчащая `оборотно[- ]сальдовая ведомость` (без учёта регистра,
«ё/е», переносов строк). Один файл может содержать несколько листов с разными ОСВ → несколько `document`.
Листы без признаков ОСВ/ББ/ОФР — пропускать с warning, не падать.
2. **Найти заголовок таблицы** по содержимому: строка(и), где встречаются одновременно «Сальдо на начало…»,
«Оборот(ы) за период», «Сальдо на конец…» (с вариантами: «Нач. сальдо», «Обороты», «Кон. сальдо», «Дебет/Кредит»,
«Дт/Кт», «Д-т/К-т»). Заголовок может занимать 1–4 строки с merged-ячейками; группировки могут быть как под «Счет»
в первой колонке, так и строкой «Субконто», так и перечислением через запятую (вариант 2).
3. **Определить колонки сумм по тексту**, а не по позиции: для каждой из трёх групп («начало», «оборот», «конец»)
найти подзаголовки «Дебет»/«Кредит» в следующей строке под ней (с учётом merged-диапазонов). Допустимы ОСВ без
«начала» (только обороты и конец), с одной свёрнутой колонкой сальдо (знак = сторона), с дополнительными
колонками («Количество», «Валюта», «ИНН», «КПП», «Код»). Неизвестные колонки — сохранять в `extra` строки, не терять.
4. **Определить колонку текста строки** — первая непустая слева от сумм (обычно A, но не обязательно); ИНН — колонка с
заголовком `ИНН` **или** колонка, где ≥ 80 % непустых значений — 10/12 цифр.
5. **Определить иерархию строк** по outline level Excel; если уровней нет (файл пересохранён, csv, другая система) —
по отступу (ведущие пробелы / `alignment.indent`), по жирности счёта, по шаблону кода счёта и, в крайнем случае, по
факту «сумма детей = родитель». Стратегии применяются каскадом, выбранная стратегия пишется в `metrics.warnings`.
6. **Шапку документа** (организация, счёт, период, «выводимые данные») искать регулярками по всем ячейкам выше заголовка
таблицы; отсутствие любого поля — не ошибка, а `null` + warning (ядро попросит аналитика уточнить при проверке).
7. **Самопроверка результата** (контрольные суммы §1.4) — единственный способ понять, что структура угадана верно.
Если тождества не сходятся — не «подгонять», а вернуть `needs_check` с диагностикой, какой шаг не уверен.
8. Правила парсера — **данные, а не код**: словари синонимов заголовков, группировок, периодов лежат в
`tabular-parser/rules/*.yaml` и расширяются без релиза. Новый формат ОСВ = новая фикстура + при необходимости новый
синоним, а не новая ветка `if`.
Тесты парсера строятся по принципу «свойств»: для любого входного файла результат удовлетворяет инвариантам §1.4,
число строк-контрагентов ≥ 0, `Итого` = корень. Фикстуры — регрессия, не оракул формата.
### 1.1 Шапка — два варианта 1С (как **примеры**, не исчерпывающий список)
**Вариант 1** (все наши фикстуры):
```
r1 <Наименование организации> ← владелец ОСВ (сверить с legal_entity → document.counterparty_check)
r2 Оборотно-сальдовая ведомость по счету 60 за 1 квартал 2025 г. ← счёт после «по счету», период после «за»
r4 Выводимые данные: БУ (данные бухгалтерского учета) ← data_type шапки
r7 Параметры: | Валюта БУ, НУ: RUB (или «руб.»)
r9 Счет | Сальдо на начало периода | Обороты за период | Сальдо на конец периода
r10 Контрагенты | [ИНН] | Дебет | Кредит | Дебет | Кредит | Дебет | Кредит
r11 Договоры ← может отсутствовать
```
Группировки — отдельные строки первой колонки блока заголовка (`Счет`, `Подразделение`, `Контрагенты`, `Договоры`),
**порядок строк = порядок вложенности**. Ячейка `ИНН` в той же строке, что `Контрагенты` → `has_inn = true`.
**Вариант 2** (встречается у других клиентов):
```
Оборотно-сальдовая ведомость по счету 60
Период: 1 квартал 2026 г.
Детализация по субсчетам, субконто: Контрагенты, Договоры ← группировки через запятую, первая неявно ACCOUNT
Выводимые данные: сумма
Субконто | Сальдо на начало периода | Оборот за период | Сальдо на конец периода
| Дебет | Кредит | Дебет | Кредит | Дебет | Кредит
```
Нормализация значений шапки в типы:
| Текст в шапке | `group_type` |
|---|---|
| Счет, Счета, Субконто (первая) | ACCOUNT |
| Подразделение, Подразделения | DEPARTMENT |
| Контрагент, Контрагенты | COUNTERPARTY |
| Договор, Договоры, Договоры контрагентов | CONTRACT |
| прочее (Валюта, Номенклатура, Статьи…) | OTHER |
Результат шапки → `recognition_job.group_schema` (по листу): `{"locator":"sheet:Лист_1","account":"60",
"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)`.
### 1.2 Колонки сумм
Заголовки сумм объединены (merged) и позиции колонок плавают между файлами даже одной организации. Определять по тексту
(§1.0 п. 3). Всё правее последней распознанной колонки сумм — не часть таблицы (в фикстурах там встречаются посторонние
числа) — складывать в `extra`, в суммы не брать. Числа могут быть numeric **или** текстом («1 234 567,89», «(1 234)»,
«-», «—», неразрывные пробелы) — нормализовать в Decimal; пустая ячейка/прочерк = NULL (не 0). Отрицательные возможны.
### 1.3 Строки
Для каждой строки табличной части: `row_number` (исходный номер), `level` (= outline level Excel; в xls — `rowinfo_map`),
`row_value` (текст первой колонки), шесть сумм, ИНН (если колонка есть; оставить только цифры, иначе NULL),
`data_type` (строки «USD», «Вал.», «Кол.» под контрагентом → CURRENCY/QUANTITY; по умолчанию — из шапки).
Классификация строки (п. 6–10 «Парсинг ОСВ»):
1. Строка — счёт, если текст матчится `^\d{2,3}(\.\d{1,2}){0,2}(,\s.*)?$` (с опциональным названием после запятой:
«60.01, Расчеты с поставщиками») → ACCOUNT, `account_code` (нормализованный), все дочерние строки наследуют счёт.
2. Не-счёт: позиция в ветке = количество не-счётных предков + 1 → соответствующая группировка из `group_schema`
(после последнего ACCOUNT). Увеличение уровня — ребёнок, равный — сосед, уменьшение — конец группы.
3. После COUNTERPARTY дочерние продолжают классифицироваться по схеме (CONTRACT, затем OTHER).
4. Строка «Итого» (level 0, последняя) — контрольная сумма всего документа; не сохранять как `osv_entry`
**[открыто №2]**, но использовать для сверки.
5. Все остальные строки сохраняются (п. 11 «Парсинг ОСВ») с `parent_entry_id` **[открыто №2]**.
Нормализация кода счёта: `60.1 → 60.01`, `60.01.1 → 60.01.1`, `76.7.1 → 76.07.1`; `account_root` = первые две-три цифры
(`001` — забалансовый). `account_id` = точное совпадение с `chart_of_account.code`, иначе NULL (администратор добавит).
### 1.4 Контрольные суммы парсера
- По каждой строке: `opening_debit − opening_credit + turnover_debit − turnover_credit == closing_debit − closing_credit` (±0.01).
- По каждому родителю: суммы родителя = Σ детей того же `data_type` (по каждой из шести колонок). Строки CURRENCY/QUANTITY
в суммирование не входят.
- Строка счёта верхнего уровня == «Итого».
Нарушение → `recognition_job.status = needs_check`, `document.status = NEEDS_CHECK`, причина — в `metrics.warnings`.
### 1.5 Маппинг периода (FNR01) → `period_quarter`, `is_cumulative`
| Текст | `period_quarter` | `is_cumulative` |
|---|---|---|
| 1 квартал, I кв., 3 месяца, январь-март, 01.01–31.03 | Q1 | true |
| 2 квартал, II кв. | Q2 | **false** [открыто: 1С может считать нарастающим] |
| полугодие, 1 полугодие, 6 месяцев, январь-июнь | Q2 | true |
| 3 квартал, III кв. | Q3 | **false** [открыто] |
| 9 месяцев, январь-сентябрь | Q3 | true |
| 4 квартал, IV кв. | Q4 | **false** [открыто] |
| год, за 2025 год, 12 месяцев, годовая | Q4 | true |
Практика на фикстурах: ОСВ «за 3 квартал 2024» у обеих компаний — **квартальные** (opening ≠ сальдо на 01.01), а
ОСВ «за 2024 г.» — годовые. Признак можно проверить по данным: если есть ОСВ Q1 того же года и
`Q3.opening == Q1.opening` → нарастающая.
Сверка периода документа с периодом версии → `document.period_check` (MATCH / MISMATCH / UNKNOWN); загрузка не блокируется.
---
## §2. Приведение периодов (DOC-259006)
Комплект (ББ + ОФР + ОСВ) за один период нарастающим итогом. Если ОСВ квартальные (`is_cumulative = false`), перед
консолидацией строится **расчётное представление** (не хранится):
```
opening_* = opening_* первой ОСВ года (Q1)
turnover_* = Σ turnover_* по кварталам Q1..Qn
closing_* = closing_* последней ОСВ (Qn)
```
Ключ соединения строк между кварталами — (счёт, контрагент, [договор]). **[открыто]**: поведение при отсутствии
промежуточного квартала; агрегировать на уровне контрагента или договора.
---
## §3. Сводная ОСВ и сопоставление контрагентов (DOC-221278, FNR01 п. 9)
Вход: `consolidation_version_id`. Периметр — `consolidation_member` версии (не текущий `related_group_member`!).
1. Для каждой компании периметра: `document` типа OSV со статусом CONFIRMED за `year/quarter` версии, входящие в
`consolidation_document` версии (если версия ещё не зафиксировала документы — последние подтверждённые).
2. Строки `osv_entry` этих документов с `group_type = COUNTERPARTY`; `data_type IN (BU, SUM)`.
3. Строки **не агрегируются**: сто строк по 60.01 у компании — сто строк в своде. `counterparty_name/inn` показываются
как в файле.
4. Резолв контрагента (при маппинге результата и повторно при построении свода, если `counterparty_id IS NULL`):
ИНН → `legal_entity.inn`; иначе точное имя → `legal_entity.name`; иначе `normalize(name)` →
`legal_entity_alias.normalized_alias`. Несколько кандидатов → NULL (ручное сопоставление).
`normalize` = lower, trim, убрать пунктуацию/кавычки, убрать ОПФ (ООО, АО, ПАО, ИП…), схлопнуть пробелы.
5. Признак контрагента: ГСК / ГК / Внешний (см. `04-data-model-guide.md`).
6. Количество строк свода = количеству отобранных `osv_entry`.
**Ручное сопоставление** (алгоритм в DOC-221278): список кандидатов = компании ГСК и ГК; поиск по ИНН/имени/алиасам;
выбор → проверка дубля `normalized_alias` у другой компании (предложить перенести или отменить) → создать
`legal_entity_alias` → `osv_entry.counterparty_id = выбранное`; `counterparty_name/inn` не трогать →
`audit_event(OSV_COUNTERPARTY_MAP)` → автоприменить к другим строкам версии с тем же именем, если `counterparty_id`
пуст, алиас однозначен и ИНН строки не противоречит.
Наблюдение по фикстурам: у «Счастливочной» контрагент «Русский бисквит» идёт **с ИНН 3528052718** в колонке E, а у
«Русского бисквита» ОСВ без колонки ИНН — контрагент «Пекарня Счастливочная ООО» только текстом → резолв по алиасу.
---
## §4. Правила элиминации — пары счетов (Прил. 3 ТЗ, DOC-223044, FNR03)
Хранятся как данные: `elimination_rule` (+ `elimination_rule_account`), версионируются; версия фиксируется в
`consolidation_version.elimination_rules_version`. Seed-набор для ББ (`statement_type = balance`, `measure = closing`):
| № | Статья А (Дт) | Счета А (closing_debit) | Статья Б (Кт) | Счета Б (closing_credit) | mode | Примечание |
|---|---|---|---|---|---|---|
| 1 | 1150 ОС | 03, 76.07.9 (01 — резервный) | 1450 / 1550 Прочие обязательства | 76.07.1 | manual | лизинг/аренда внутри группы; у Б: 76.07.1 − 76.07.5 − 76.07.9. Если у А нет ОСВ 03 — результат в строку «ППА» [открыто] |
| 2 | 1170 ФВ долгосрочные | 58.01 | 1310 / 1350 УК / добавочный | 80, 83 | manual | **гудвилл**; если у Б нет ОСВ 80/83 — элиминируется 58.01 А; `1310(Б) − 58.01(А) > 0` → строка «Гудвилл» |
| 3 | 1170 ФВ долгосрочные | 58.03 | 1410 Займы долгосрочные | 67.03 | min | |
| 4 | 1240 ФВ краткосрочные | 58.03 | 1510 Займы краткосрочные | 66.03 | min | |
| 5 | 1230 Дебиторка | 76 (кроме 76.07.5, 76.07.9) | 1510 Заёмные краткосрочные | 66.04, 67.04 | min | проценты |
| 6 | 1230 Дебиторка | 60.02, 60.22, 60.32, 62.01, 62.03, 62.21, 62.31, 76 (кроме 76.07.5, 76.07.9) | 1520 Кредиторка | 60.01, 60.03, 60.21, 60.31, 62.02, 62.22, 62.32, 76 (кроме 76.07.1) | min | основная пара |
| 7 | 1230 Дебиторка | те же, что № 6 | 1410 Заёмные долгосрочные | 67 | min | |
| 8 | 1230 Дебиторка | те же, что № 6 | 1510 Заёмные краткосрочные | 66 | min | |
Правило 5 % для пар: суммы равны → маппим; расхождение ≤ 5 % (к меньшей) → берём `min`; > 5 % → подсветка, авто не
маппится, ждёт корректировки (`ADJUSTMENT`).
ОФР (`statement_type = income`, `measure = turnover`), из FNR03 — **[открыто]**: формализовать с методологами:
| Статья | Сторона А | Сторона Б | mode / НДС |
|---|---|---|---|
| 2110 Выручка | Кт 90.01 (с НДС, приоритет) либо Дт 62.01/62.21/62.31 по контрагенту | Кт 60.01/60.21/60.31 + 76 (кроме 76.07.*, 76.03.*, плюс 76.07.2) | `full` — берётся сторона А; без НДС = с НДС / (1 + средневзвешенная ставка/100) |
| 2120 Себестоимость | — | — | элиминация = ВГО по 2110 (зеркально, без НДС) |
| 2310 Доходы от участия | Дт 76.03 | Кт 76.03 | сторона А |
| 2320 Проценты к получению | Дт 76.03/76.05/76.08/76.09 | Кт 66.02/.04/.06/.22/.24/.26, 67.02/.04/.06/.22/.24/.26 | min |
| 2330 Проценты к уплате | Кт чётных субсчетов 66/67 + 76.07.5/76.08/76.28/76.38 | Дт 76.03/.05/.08/.09 | min |
Дополнительные ручные корректировки ОФР (ТЗ 6.4, `correction.reason_code`): нереализованная выручка, акцизы
(`osv_entry.is_excisable` для 90), агентское вознаграждение, внутригрупповая амортизация по аренде, проценты по
внутригрупповым займам, доходы от участия, прочее.
---
## §5. Автоматическое сопоставление ВГО (DOC-223044)
Границы: только `closing_debit` / `closing_credit`; только компании периметра версии; пары счетов — из §4; **1:1**;
строка участвует в одном сопоставлении целиком.
```
для каждой строки a компании A с counterparty_id = B ∈ периметр, group_type = COUNTERPARTY, data_type ∈ (BU, SUM):
сторона a = closing_debit, если > 0, иначе closing_credit
кандидаты = строки b компании B с counterparty_id = A, противоположная сторона,
(счёт a, счёт b) входит в допустимую пару правила r (для каждого r — отдельный набор)
1. точное 1:1 (сумма a == сумма b, одна пара счетов) и единственный вариант → матч, обе строки выбывают
2. несколько точных по РАЗНЫМ правилам → не матчим, вниз таблицы на ручное
3. нет точного: по каждому правилу — минимальное |a − b|; выбираем правило с минимальным расхождением;
равное минимальное по разным правилам → не матчим (ручное)
результат пары: rule, A, B, сумма A, сумма B, deviation_amount = |A−B|, deviation_percent = dev / min(A,B) * 100,
исходные osv_entry.id обеих сторон
```
Результат **не хранится** (DECISION-5) — возвращается как структура для шахматки, корректировок и КФО. Ручное решение по
паре — `correction` с естественным ключом (`rule_id`, `legal_entity_id`, `counterparty_id`).
Открыто: агрегировать ли несколько строк одной компании по одному контрагенту и счёту перед матчингом (FNR03 говорит
«несколько строк суммируются», DOC-223044 — «одна позиция = одна строка»). Рекомендация: агрегировать по
(компания, контрагент, счёт, сторона), сохраняя список `osv_entry.id` как детализацию.
---
## §6. Шахматка и результирующая таблица (DOC-221277)
Вход: версия + набор дебетовых счетов (`kind ∈ A, AP`) и кредитовых (`kind ∈ P, AP`); предустановки: «58.03 vs
67.03 + 66.03», «60 + 62 + 76 vs 60 + 62 + 76», «все счета».
Строки/столбцы — компании периметра; диагональ пуста.
```
amount_a(A,B) = Σ closing_debit строк A с counterparty_id = B по дебетовым счетам
amount_b(A,B) = Σ closing_credit строк B с counterparty_id = A по кредитовым счетам
variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amount_b) * 100; > 5 % — подсветка
ячейка: amount_a / amount_b / (variance)
```
Фильтры строк — как в §3 (COUNTERPARTY, BU/SUM, `account_id IS NOT NULL`).
Результирующая таблица «компания против группы» по статьям: Дебиторка (Дт 60.02, 60.22, 60.32, 62.01, 62.03, 62.21,
62.31, 76 кроме 76.07.5/76.07.9), Кредиторка (Кт 60.01, 60.03, 60.21, 60.31, 62.02, 62.22, 62.32, 76 кроме 76.07.1),
ФВ (Дт 58.01, 58.03), Долгосрочные займы (Кт 67, 67.03), Краткосрочные (Кт 66, 66.03, 66.04); колонка «Итого по группе».
---
## §7. Логические проверки ББ/ОФР (DOC-221281, FNR03 шаг 1)
Выполняются по **каждой колонке-дате** отдельно; ничего не меняют; результат не хранится.
ББ: `1700 − 1600 = 0`; `1100 = 1110 + 1150 + 1160 + 1170 + 1180 + 1190` (+1120, 1130, 1140 если есть);
`1200 = 1210 + 1220 + 1230 + 1240 + 1250 + 1260`; `1300 = 1310 + 1320 + 1340 + 1350 + 1360 + 1370`;
`1400 = 1410 + 1420 + 1430 + 1450`; `1500 = 1510 + 1520 + 1540 + 1550`; `1600 = 1100 + 1200`; `1700 = 1300 + 1400 + 1500`.
ОФР: `2100 = 2110 − 2120`; `2200 = 2100 − 2210 − 2220`; `2300 = 2200 + 2310 + 2320 − 2330 + 2340 − 2350`;
`2400 = 2300 − 2410 + 2420` (знаки 2410/2420/2430 — по ФСБУ 4/2023 уточнить [открыто]).
Межформенная: `1370(отчётная дата) − 1370(начало года) = 2400(ОФР за период)` — при расхождении КА может акцептовать
с комментарием (`financial_statement_meta.checks_accepted_*`).
Когда: после распознавания; после «Сохранить» правок; при «Подтвердить данные». Непройдено → `document.status =
NEEDS_CHECK`; пройдено/акцептовано + нажато «Подтвердить» → `CONFIRMED` + `audit_event(DOCUMENT_CONFIRM)`.
Отрицательные значения в формах — в скобках `(2100)` → `-2100`. Значения с `confidence < 100` подсвечиваются.
---
## §8. Корректировки, гудвилл, КФО, пороги
**Корректировка** (DOC-227278, ТЗ 6.8): запись на пару (правило, компания 1, компания 2) и форму (ББ/ОФР); `debit_line_code
/ debit_adjustment`, `credit_line_code / credit_adjustment`; распределение суммы одной стороны по нескольким строкам —
дочерние записи (`parent_correction_id`, глубина 1). Контроль: `Σ debit_adjustment` ≈ `Σ credit_adjustment` (±5 %),
иначе ВГО распределено не полностью. Комментарий обязателен. История — `audit_event(CORRECTION_*)`, `version_no++`.
**Гудвилл** (DOC-259025, ТЗ 6.7): по каждой паре ГО→ДО периметра:
`FV = Σ closing_debit (58.01, контрагент ДО)`; `UK = 1310 ББ ДО на отчётную дату` (FNR03: Кт 80 + Кт 83, при
отсутствии — 1310); `share` — ручной ввод; `participation = (1310 [+ 1350?]) × share / 100`;
`goodwill = FV − participation` (может быть < 0); `include_in_kfo` + комментарий обязателен. Положительный итог —
строка `GOODWILL` в разделе I КФО.
**КФО** (ТЗ 6.4, Прил. 10): по каждому коду строки ББ/ОФР:
`sum_amount = Σ по компаниям` (значения отчётной колонки ББ / ОФР за период, в тыс. руб.) →
`eliminated_auto` (по §4–5, пересчёт в тыс. руб.) → `eliminated_manual = Σ correction APPLIED` → `goodwill_adjust` →
`consolidated_amount`; `vgo_amount = eliminated_auto + eliminated_manual`; разрез `kfo_result_line_company` (`amount`,
`share_percent` = доля компании в консолидированном показателе). Эталон колонок — `Консолидация_Группа РБ.xlsx`, лист
`01.10.2024`: «значение на дату | в т. ч. взаимные обязательства | очищенные показатели | доля в показателе по группе».
**Пороги** (ТЗ 6.8, FNR03 шаги 4–5), `kfo_result.threshold_*`:
- ББ: если `1300 < 0.1 × 1600` → Σ расхождений ≤ 1 % × 1600 (ВБ), иначе ≤ 10 % × 1300 (ЧА).
- ОФР: если `2200 / 2110 < 10 %` или отрицательна → ≤ 1 % × 2110 (выручка), иначе ≤ 10 % × 2200.
Превышение → `threshold_justification` обязателен (принятие риска), подсветка.
**Сравнение с КФО клиента** (`scenario = CONFIRM`): `client_kfo_line` построчно: `client_amount`, `client_vgo_amount`
(ручной ввод), `bank_amount`, `bank_vgo_amount` (из `kfo_result_line`), `deviation_percent` к наименьшему, причина.
---
## §9. Статусные модели
### 9.1 КФО (`consolidation_status`, DOC-037734 + ТЗ 6.10)
| Из | В | Кто | Когда |
|---|---|---|---|
| — | INITIATED | КА РФ / КА ГО | создана карточка, зафиксирован периметр, сценарий, подход |
| INITIATED | DOCUMENTS_PROCESSING | система/КА | загрузка и распознавание |
| DOCUMENTS_PROCESSING | DATA_CORRECTION | КА | есть документы NEEDS_CHECK |
| DATA_CORRECTION | CONSOLIDATION_RF | КА РФ | все обязательные документы CONFIRMED; инициатор — РФ |
| DATA_CORRECTION | CONSOLIDATION_GO | КА ГО | инициатор — ГО |
| CONSOLIDATION_RF | ADJUSTMENT | система | расхождение ≥ 5 % в парах (подстатус) |
| CONSOLIDATION_RF | VERIFICATION_RF | КА РФ | «отправить на верификацию» |
| VERIFICATION_RF | CONSOLIDATION_GO | РКА РФ | подтвердил |
| VERIFICATION_RF | CONSOLIDATION_RF | РКА РФ | вернул (комментарий обязателен) |
| CONSOLIDATION_GO | CONSOLIDATION_RF | КА ГО | вернул на доработку (комментарий) |
| CONSOLIDATION_GO | VERIFICATION_GO | КА ГО | если инициатор — КА ГО, верифицирует РКА ГО [открыто] |
| CONSOLIDATION_GO / VERIFICATION_GO | VERIFIED | КА ГО / РКА ГО | внесены № и дата решения УОБ; версия read-only; пишется `ex_snapshot` |
| любой нефинальный | INITIATED | КА | нужен другой периметр/сценарий/комплект |
| любой нефинальный | CANCELLED | КА | аннулировано |
| VERIFIED | — | — | только «новая версия» (`previous_version_id`) |
| из макета | REJECTED, ARCHIVED | | ANL-035 — подтвердить с заказчиком |
Каждый переход → `consolidation_status_history` + `audit_event(CONSOLIDATION_STATUS_CHANGE)`.
### 9.2 Файл (`file_status`): UPLOADED → PROCESSING → PROCESSED | ERROR; любой → SUPERSEDED (замена файлом целиком,
`superseded_by_file_id`, `version_no + 1`). Физическое удаление после INITIATED запрещено (ANL-020) — только замена.
### 9.3 Документ (`document_status`): создаётся ядром при маппинге результата: все значения `confidence = 100`, проверки
пройдены, период и контрагент совпали → RECOGNIZED [открыто: нужен ли], иначе NEEDS_CHECK; «Подтвердить данные» → CONFIRMED;
не разобран (нет шапки) → ERROR.
### 9.4 Задание (`recognition_status`): queued → published → processing → done | needs_check | error | timeout → dlq;
replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `reply_deadline` OCR 5 мин.
---
## §10. Роли и матрица прав (ТЗ 8.1, DOC-036960) — seed для `role_permission`
| Роль (`role.code`) | DIRECTORY | DOCUMENT | CONSOLIDATION | KFO | EX | EXPORT | USERS | AUDIT |
|---|---|---|---|---|---|---|---|---|
| ADMIN | VIEW, EDIT, DELETE, ADMIN (импорт) | — | — | — | VIEW | — | ADMIN | VIEW |
| CURATOR_GO (Куратор КП ГО) | VIEW, EDIT (ГК: создание, перезакрепление) | — | VIEW (по закреплённым ГК) | VIEW | VIEW | — | — | — |
| KA_RF | VIEW (свой РФ), EDIT (состав ГСК своей ГК) | VIEW, EDIT (загрузка, правка, подтверждение) | VIEW, EDIT (INITIATED…CONSOLIDATION_RF) | VIEW, EXPORT | VIEW | EXPORT | — | — |
| RKA (РКА РФ) | VIEW, EDIT (состав ГСК в рамках корректировки) | VIEW | VIEW, EDIT (VERIFICATION_RF) | VIEW, APPROVE | VIEW | EXPORT | — | — |
| KA_GO | VIEW, EDIT (состав ГСК, без ограничения по филиалу) | VIEW, EDIT | VIEW, EDIT (всё, что KA_RF + CONSOLIDATION_GO) | VIEW, VERIFY (№/дата УОБ), EXPORT | VIEW | EXPORT | — | — |
| EXPERT | VIEW | VIEW | VIEW | VIEW | VIEW | — | — | — |
| CLIENT_MANAGER | VIEW (свои клиенты) | VIEW, EDIT (только загрузка по своей ГК) | VIEW (статус) | VIEW | — | — | — | — |
Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят только ГК своего `regional_office`; редактирование версии — только в
допустимом статусе и при отсутствии чужой активной `consolidation_lock`; ЗО (`business_segment`) — фильтр видимости.
Матрица в ТЗ помечена «будет разработана» (сноска 29) — seed считать черновиком, редактируется администратором в UI.
---
## §11. Маппинг JSON ИС Сканирование (FNR01/FNR02) → формы
Контракт (по ТЗ и фикстурам `aep_*.json`): `file_uid`; `error {code, message} | null`
(FILE_READ_ERROR, INVALID_FORMAT, INVALID_DOCUMENT, RECOGNITION_FAILED, UNKNOWN_ERROR); `data_ocr[]` по документам:
`label.form_name` («Бухгалтерский баланс» → BALANCE, «Отчет о финансовых результатах» → INCOME), `organization.full_name`,
`inn.inn`, `units.measurement`, `date`, `table_structure.rows[]` (`row_index`, `cells[] {column_index, value, confidence}`).
Строка `rows[0]` — заголовки колонок: cells[0] «Наименование показателя», [1] «Код», [2..] даты колонок.
`confidence` — в ТЗ доли 0..1, в реальных файлах проценты 0..100 **[открыто]** — нормализовать в `numeric(5,2)` 0–100.
Маппинг: `data_ocr[i]` → `document` (BALANCE/INCOME, `locator = pages:…`) + `financial_statement_meta` (ИНН, имя,
units, report_date, okud) + `financial_statement_column` (из `rows[0].cells[2..]`, `period_date` распарсить из
«На 30 сентября 2025 г.», `is_current` = первая) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя; только
строки с кодом) + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`).
Сверки: ИНН/имя vs `legal_entity` → `document.counterparty_check`; дата vs период версии → `period_check`.
---
## §12. ББ и ОФР из Excel (tabular-parser, второй тип документа)
Источник: ТЗ 6.2 («парсинг ОСВ, бухгалтерского баланса и ОФР (эксель файлов)»), DOC-037734, DOC-221281; формат —
Приказ 66н / ФСБУ 4/2023, но на практике это и выгрузка из 1С («Регламентированный отчёт»), и шаблон банка
(`Копия Рекомендуемые формы расшифровок…`, лист `Отчетность` в `Анализ *.xlsx`), и вручную набитая таблица.
Принцип тот же, что в §1.0: **структура ищется по содержимому**, результат — тот же `statementDocument` контракта,
что и у `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`.
Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_*` для PDF и Excel; экран
«Проверка распознавания» для Excel-источника показывает слева не страницы, а рендер листа (или просто таблицу) —
UI-решение вне парсера.