diff --git a/Makefile b/Makefile index f9a0b6a..b5fbc59 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3) # Пусто — проверяются все change'и и спеки; `make validate-openspec CHANGE=` — один change CHANGE ?= # Текущая версия модели данных — по ней рисуются диаграммы (make diagrams) -DBML ?= model/data-model-v5.24.dbml +DBML ?= model/data-model-v5.26.dbml .PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-statements sync-statements statements-check check diagrams regress diff --git a/analyst/eliminations-vgo.md b/analyst/eliminations-vgo.md index 425dba3..dcd0ff7 100644 --- a/analyst/eliminations-vgo.md +++ b/analyst/eliminations-vgo.md @@ -1,7 +1,7 @@ # Элиминация ВГО: правила, расчёт, корректировки Документ для аналитика, 19.10.2026 (RSHB/arm-ka-docs#132; сопоставление — по решениям OQ-07 и OQ-173, #136). Основа — модель данных -[`model/data-model-v5.24.dbml`](../model/data-model-v5.24.dbml) и правила предметной области +[`model/data-model-v5.26.dbml`](../model/data-model-v5.26.dbml) и правила предметной области [`context/05-domain-rules.md`](../context/05-domain-rules.md) §3–§6, §8. Источник истины — они: при расхождении прав DBML и `05-domain-rules.md`, а этот документ поправь следом. Где написано наше прочтение ТЗ, а не прямая цитата, это помечено; такие места собраны в раздел 6 вопросами. diff --git a/context/00-workspace-layout.md b/context/00-workspace-layout.md index f34a435..66ac0cb 100644 --- a/context/00-workspace-layout.md +++ b/context/00-workspace-layout.md @@ -76,7 +76,7 @@ git remote add origin && git push -u origin main | Слой | Где | Статус | Правило для агента | |---|---|---|---| -| Модель данных | `docs/model/data-model-v5.24.dbml` | стабильный, версионируется | единственный источник имён и структур | +| Модель данных | `docs/model/data-model-v5.26.dbml` | стабильный, версионируется | единственный источник имён и структур | | Правила домена | `docs/context/05-domain-rules.md` | курируемый — пишет человек/агент по итогам ревью | **единственный вход алгоритмов в код**; из ТЗ напрямую не кодируем | | Открытые вопросы | `docs/context/07-open-questions.md` | живой реестр | каждое допущение в коде — `ASSUMPTION(OQ-nn)` | | Экспорт ТЗ | `docs/source/tz/` | снимок на дату из `docs/source/tz/_export.md` | читать для контекста; расхождение с §05 → вопрос, не код | diff --git a/context/03-architecture.md b/context/03-architecture.md index 327c58e..3edf89f 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -1,4 +1,4 @@ -# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.24) +# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.26) ## Компоненты (= репозитории, см. `00-workspace-layout.md`) @@ -251,7 +251,7 @@ api/src/ # машины статусов, policy — pydantic-модели на входе и выходе services/ # use-cases: оркестрация domain + repositories + outbox + audit_event repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка) - models/ # ORM 1:1 с DBML v5.24; имена таблиц/колонок/enum — как в DBML + models/ # ORM 1:1 с DBML v5.26; имена таблиц/колонок/enum — как в DBML schemas/ # pydantic DTO API /api/v1 core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент), # logger, exception_handlers @@ -341,7 +341,7 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро `version_table_schema` и `include_name` Alembic). Следуем шаблону, не `public`. - Alembic берёт URL из настроек, не из `alembic.ini`; ревизии — `api/alembic/versions/`, имя файла стандартное `_.py`. Автогенерация — только черновик: итоговую ревизию сверяет с DBML `api/scripts/check_dbml_vs_orm.py`. -- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.24. +- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.26. ### Инфраструктурные клиенты diff --git a/context/04-data-model-guide.md b/context/04-data-model-guide.md index aa507c4..e2751c2 100644 --- a/context/04-data-model-guide.md +++ b/context/04-data-model-guide.md @@ -1,6 +1,6 @@ -# 04. Как читать модель данных v5.24 +# 04. Как читать модель данных v5.26 -Файлы: `docs/model/data-model-v5.24.dbml` (источник истины), `docs/model/entities-v5.24.md` (смысл таблиц; предыдущие +Файлы: `docs/model/data-model-v5.26.dbml` (источник истины), `docs/model/entities-v5.26.md` (смысл таблиц; предыдущие версии `data-model-v5.13…v5.20.dbml` и `entities-v5.13…v5.20.md` остаются как история), `docs/model/diagrams/part01…09.{svg,png}` (диаграммы v5.21 по частям из таблицы ниже; перерисовываются из DBML командой `make diagrams` в корне репозитория docs — скрипт `scripts/gen_model_diagrams.py`, нужен npx; при новой версии DBML @@ -20,6 +20,8 @@ | 08 Корректировки и КФО | `correction`, `vgo_assignment`, `goodwill`, `kfo_result`, `kfo_result_line`, `kfo_result_line_company`, `client_kfo_line` | ручные решения и результат | | 09 Комментарии, аудит, ЕХ | `comment`, `audit_event`, `ex_snapshot`, `ex_snapshot_member`, `ex_snapshot_item` | полиморфные комментарии, append-only журнал, замороженные срезы | +**v5.26:** расчёт КФО (§8 «КФО», AURORA-1546): `kfo_result_line_company` + `client_amount`, `vgo_amount` («по данным клиента», «в т. ч. взаимные» по компании); `kfo_result` + `warnings` и сводка корректировок; `audit_action.KFO_EDIT`; режим правила `min_or_full` (ББ 2). Номер v5.25 — PR #203 (факты качества первички), ещё не слит. + **v5.24:** новая таблица `vgo_assignment` — ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения по правилам ВГО (§5): привязка к правилу / снятие, «не учитывать в КФО» у нераспределённой строки (AURORA-1751). **v5.23:** `consolidation_version.elimination_rules_version` — `not null`: версия правил элиминации, действующая на дату создания версии консолидации, фиксируется при создании; существующим версиям проставляется миграцией (AURORA-1536). diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 6059607..a8bd42b 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -707,6 +707,8 @@ closing_* = closing_* строки Qn deviation_amount = |ΣA − ΣB|; deviation_percent = deviation_amount / min(ΣA, ΣB) * 100 mode = min: расхождение ≤ 5 % → элиминация_r = min(ΣA, ΣB); > 5 % → подсветка, элиминация_r = 0, ждёт корректировки mode = manual: элиминация_r = 0 — только корректировка / гудвилл (§8) +mode = min_or_full (правило 2, DOC-274230; seed правится в AURORA-1546): обе стороны > 0 → min(ΣA, ΣB) без порога; + ΣB = 0 → ΣA; ΣA = 0 → 0; компании 2 — каскадом по строкам article_2_line (§8 «КФО», п. 4) ``` **Знак** (решение владельца 23.10.2026): суммы сторон — **со знаком**, как в шахматке §6 (одно правило и одна пара дают одинаковые ΣA и ΣB на обоих экранах). Отрицательное сальдо строки (§1: возможно) — встречная позиция, например аванс или @@ -941,7 +943,14 @@ OQ-145); тем же правилом сервисы разрешают ничь - **Контроль распределения** (`CorrectionBalanceOut`): `debit_total`/`credit_total` — суммы сохранённых статей; `difference`/`difference_percent` между ними, `remainder_1`/`remainder_2` — сумма стороны агрегата минус сохранённая статья. Это только отображаемый контроль — корректировку можно сохранить, даже если стороны не сошлись; в КФО по - выбранным статьям уходит меньшая сумма (ответ п. 8). + выбранным статьям уходит меньшая сумма (ответ п. 8): при заполненных обеих сторонах каждая исключает меньший из + итогов, снимая его со своих статей по порядку (блок «КФО», п. 5). Применённая корректировка **заменяет** автосумму + агрегата, а не прибавляется к ней. +- **Автосумма агрегата** (`eliminated_amount`) — по режиму правила, **одной функцией домена с КФО** (блок «КФО», п. 4): + `min` — меньшая сторона в допуске, иначе 0; `manual` — 0; `min_or_full` (правило 2 после правки seed в AURORA-1546) + — меньшая сторона при обеих > 0, `amount_1` при пустой стороне 2, порога нет: `exceeds_threshold = false`, статус + `AUTO` при обеих сторонах, `ONE_SIDED` при пустой стороне 2 (`eliminated_amount = amount_1`). Сейчас + `services/correction_source.py` знает только `manual` и «остальное как `min`» — правится вместе с seed. - **Порог отклонения агрегата** (`exceeds_threshold`, не путать с контролем выше): `deviation_percent = |ΣA − ΣB| / min(ΣA, ΣB) × 100` сторон агрегата (§5, п. 1/2a); реализовано **строго больше 5 %** (R-35, `domain/corrections.py`, `arm-core#215`/Task 8) — ровно 5,00 % в допуске. ТЗ/DOC-272613 читались как «от 5 % включительно»; аналитик на это не @@ -973,21 +982,199 @@ OQ-145); тем же правилом сервисы разрешают ничь тоже); в КФО — `Σ G > 0` строк, где `use_goodwill` = действующая ∧ отмечена ∧ доля и комментарий заполнены ∧ `G > 0`, и только при `consolidation_version.goodwill_enabled` (переключатель на вкладке, OQ-27). Доли в итогах не складываются; сумма заполненных долей разных ГО в одной ДО > 100 % — предупреждение, не ошибка. -- **КФО** (AURORA-1546, ещё не реализовано): для пары с `use_goodwill` — ГО `1170 − ФВ`, ДО `1310 − P`, ГО строка - `GOODWILL` `+ G` (служебный код, ASSUMPTION OQ-37), обычный ББ-2 по этой паре не применяется; при `G ≤ 0` — обычный - ББ-2, разница в 1370 не переносится. Изменение ввода, переключателя или источников делает результат КФО устаревшим. +- **КФО** — блок «КФО» ниже, п. 8: ветка гудвилла идёт по паре этой таблицы независимо от агрегатов корректировок; + у пары с `use_goodwill` — ГО `1170` на ФВ, ДО `1310` на P, строка `GOODWILL` на G, агрегат правила 2 той же пары не + применяется; при `G ≤ 0` или выключенном `goodwill_enabled` — обычный агрегат правила 2. Изменение ввода, + переключателя или источников делает результат КФО устаревшим. -**КФО** (ТЗ 6.4, Прил. 10): по каждому коду строки ББ/ОФР: -`sum_amount = Σ по компаниям` (значения отчётной колонки ББ / ОФР за период, в тыс. руб.) → -`eliminated_auto` (Σ сумм элиминации троек, §5 п. 1; пересчёт в тыс. руб.) → `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.4, 6.8, Прил. 10; AURORA-1546 — расчёт, AURORA-1547 — экран, AURORA-1417 — пересчёт; ещё не реализовано; +контракт — `dev/api-contract-kfo.md`). Источники, в порядке приоритета: DOC-274230 «Правила пар счетов» (Лысенко, +27.10.2026), DOC-259025 «Гудвилл» (03.11), DOC-272613 «Корректировка данных агрегатом» (04.11); черновик команды +DOC-274636 «Расчёт КФО» и DOC-274647 «Экран: КФО» — там, где страниц Лысенко нет (решение владельца 28.10.2026: страницы +аналитика важнее черновика). Развилки, которые решает заказчик, закрыты вариантом черновика как стартовым и помечены +OQ — менять по ответу. -**Пороги** (ТЗ 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` обязателен (принятие риска), подсветка. +**Единицы.** Результат КФО хранится и отдаётся в **тыс. руб.** с двумя знаками. Суммы ОСВ и агрегатов — в рублях; +в тысячи переводятся один раз — на ячейке «в т. ч. взаимные» (компания × строка, п. 6). + +1. **Строки.** Бланк ББ и ОФР — полный вариант (`full`) той редакции, которую ядро выбирает по **отчётной дате версии** + (§11.9: ≤ 31.12.2024 — `66n`, позже — `fsbu4`; перечни — `contracts/statement-code-orders.yaml`, наименования — + словарь имён ядра). Плюс служебная строка `GOODWILL` (п. 8). Выводятся все строки бланка, в том числе нулевые. + Строки форм компаний вне бланка и строки без кода в КФО не входят (решение 17.10.2026; предупреждение + `LINE_OUTSIDE_BLANK`). Перечень дополнительных строк FNR03 («ППА», «Износ ОС», «Дополнительная информация») — OQ-180. +2. **«По данным клиента» (`client_amount`).** По каждой компании периметра (`consolidation_member`) — подтверждённый + (`CONFIRMED`) документ ББ / ОФР, привязанный к версии (`consolidation_document`). Колонка: ББ — на конец отчётного + квартала, ОФР — за период с начала года по конец квартала; по дате колонки, не по `is_current` (как УК гудвилла). + Строки со статусом `ok`; повторы кода суммируются. Единицы формы — словарём единиц `arm_statements` → тыс. руб.: + «тыс. руб.» — как есть, «руб.» — / 1000, «млн руб.» — × 1000, до двух знаков `ROUND_HALF_UP`. + - **Знак строк расходов.** Формы хранят расходы по-разному (§7, §11.9: скобки, минус или «без знака»). Строки + расходов бланка — **2120, 2210, 2220, 2330, 2350** — приводятся к одной конвенции: `client_amount = −|значение|`. + Остальные строки — как в форме (1370, 2300, 2400 и прочие бывают любого знака по существу). 2410 / 2411 (налог + на прибыль) в §7 стоят в `abs_terms`, но их знак не определён (OQ-12) — берутся как в форме. + - Нет формы → значения компании 0, `FORM_MISSING`; две и более подтверждённые формы одного типа → 0, + `FORM_AMBIGUOUS`; единицы не распознаны → 0, `FORM_UNITS_UNKNOWN`. Значение из ОСВ вместо формы — OQ-182. + - КФО считается и при неполном комплекте — по тому, что подтверждено и привязано, с `FORM_MISSING` (DOC-274636, + допущение 17; OQ-194). +3. **Агрегаты.** Взаимные суммы КФО берёт из **агрегатов таблицы корректировок** (§5 п. 2a, `VgoService`): тройка + (правило, компания 1, компания 2) с суммами сторон `amount_1`, `amount_2` (рубли) после ручных решений + `vgo_assignment`. Отдельного обхода ОСВ в КФО нет: что аналитик видит в таблице корректировок, то и вход КФО. + Нераспределённые строки ни в один агрегат не входят и в КФО не исключаются (`UNALLOCATED_ROWS` — число таких строк + без отметки «не учитывать в КФО»). +4. **Исключения агрегата.** Каждый агрегат даёт список исключений `(компания, код строки, сумма в рублях ≥ 0)`. Есть + применённая корректировка (`APPLIED`) — п. 5, иначе **автосумма** по режиму правила (`elimination_rule.elimination_mode`): + + | Режим | Автосумма | Куда | + |---|---|---| + | `min` (ББ 3–8) | `min(amount_1, amount_2)`, если обе стороны > 0 и отклонение в допуске (порог — блок «Корректировка», OQ-178); иначе 0 | `article_1_line` компании 1, `article_2_line` компании 2 | + | `min_or_full` (ББ 2) | `amount_1 ≤ 0` → 0; иначе `amount_2 > 0` → `min(amount_1, amount_2)`, `amount_2 ≤ 0` (пусто или встречная позиция) → `amount_1`; порога нет (DOC-274230, правило 2) | `article_1_line` компании 1; компании 2 — каскадом по `article_2_line` | + | `manual` (ББ 1) | 0 — только корректировка | — | + | режимы ОФР | по правилам ОФР (DOC-274230, AURORA-1999, §5); до неё агрегатов ОФР нет — `INCOME_RULES_MISSING` | по правилу | + + **Каскад** (составная статья «a/b/c»): сумма снимается со строки `a` компании в пределах её доступного остатка + («по данным клиента» строки в рублях минус уже снятое с неё другими агрегатами, не ниже 0), остаток — со строки `b`, + затем `c`; не уместившееся — целиком на последнюю строку (DOC-274230: «если остаётся положительный остаток, он + исключается из строки 1370»). **Порядок:** сначала исключения гудвилл-пар (п. 8), затем агрегаты в порядке + `rule_no`, ИНН компании 1, ИНН компании 2; доступный остаток строки уменьшают все уже сделанные исключения — + гудвилл, автосуммы и корректировки. + + Seed правил ББ версии 1 (`arm-core/api/seed/core/elimination_rules_v1.yaml`, правка — в AURORA-1546, на месте, без + новой версии правил: верифицированных версий нет): правило 2 — `elimination_mode: min_or_full`, + `article_2_line: "1310/1350/1370"`. **Правило 1 остаётся `manual`**: по DOC-274230 обе его суммы — у компании 2 + (Кт 76.7.1 − Дт 76.7.9 с каскадом 1150 → 1450 → 1550), а сторона агрегата устроена «компания 1 — сторона 1, + компания 2 — сторона 2»; автосумма правила 1 — после переработки seed (OQ-193), до того — корректировкой. + + Автосумма — **одна функция домена** для таблицы корректировок и КФО (блок «Корректировка», `eliminated_amount`). +5. **Корректировка заменяет автосумму.** Применённая корректировка агрегата отменяет его автосумму целиком и к ней не + прибавляется (DOC-274636, допущение 4; DOC-272613: корректировки не меняют суммы сторон). Исключения: компания 1 — + статьи `debit[]` (основная, затем дочерние), компания 2 — `credit[]`, суммы как введены. Если заполнены обе стороны + и их итоги разные, каждая сторона исключает **меньший** итог (ответ Лысенко п. 8): снимается со статей стороны по + порядку — основная, затем дочерние в порядке ввода — до исчерпания. Пустая сторона ничего не исключает, другая — как + введена (одностороннее правило 1, правила ОФР 1–3). +6. **«В т. ч. взаимные» по компании (`vgo_amount`).** Σ исключений компании по строке в рублях → тыс. руб., **до целых + тысяч, половина — от нуля** (`ROUND_HALF_UP` по модулю), со **знаком строки**: у строк расходов (п. 2, они ≤ 0) + `vgo_amount` отрицательный. На всех строках одно равенство `cleaned_amount = client_amount − vgo_amount`; исключение + приближает значение к нулю (при превышении — проходит через ноль). Округление ячейки, а не каждой тройки и не «без + округления» — OQ-184. +7. **Строка КФО.** По компании: `client_amount`, `vgo_amount`, `cleaned_amount`, `share_percent = cleaned / bank_amount + × 100` (`ROUND_HALF_UP`: хранится 4 знака, отдаётся 2; `bank_amount = 0` или `|доля| ≥ 1000 %` → `null`; бывает + отрицательной и больше 100 %). По строке: `sum_amount = Σ client`, `vgo_amount = Σ vgo по компаниям = + eliminated_auto + eliminated_manual` (manual — от агрегатов с корректировкой, auto — от агрегатов без корректировки и + гудвилл-пар; по одному агрегату они **не складываются**). Округляется одна ячейка: у компании по строке `vgo` — Σ + всех её исключений в рублях → тысячи (п. 6), manual — Σ исключений по корректировкам → тысячи тем же правилом, + **auto = vgo − manual**; так `vgo = auto + manual` и после округления. Дальше по строке — + `goodwill_adjust` (у `GOODWILL` и итогов, куда он входит), **`bank_amount = sum_amount − vgo_amount + goodwill_adjust`** + (`kfo_result_line.consolidated_amount`). + + **Итоговые строки** (1100, 1200, 1300, 1400, 1500, 1600, 1700; 2100, 2200, 2300, 2400, 2500): «по данным клиента» — + итог из формы компании, а если строки итога в форме нет (упрощённая форма, `form_variant = simplified`) — Σ «по + данным клиента» его слагаемых; «в т. ч. взаимные» — Σ `vgo_amount` слагаемых **с коэффициентом +1** (знак уже в + значениях после приведения п. 2): 1100 ← 1110…1190; 1600 ← 1100 + 1200; 1700 ← 1300 + 1400 + 1500; 2100 ← 2110 + + 2120; 2200 ← 2100 + 2210 + 2220; 2300 ← 2200 + 2310…2350; 2400 и 2500 — слагаемые по формулам §7 своей редакции + (66н / ФСБУ 4), но **без вычитания** (формулы §7 с минусом к КФО не применяются); строки «в т. ч.» (2421 и подобные) + — не слагаемые; `goodwill_adjust` — Σ слагаемых (то есть G у 1100 и 1600); очищенные — разность. Итог КФО — подписанный + клиентом итог минус взаимные, а не сумма своих строк (OQ-183). Доли компаний у 1100/1600 в сумме меньше 100 % на долю + гудвилла — у него нет разреза по компаниям. `deviation_amount` у итоговых строк — `null`. +8. **Гудвилл** (DOC-259025, блок «Гудвилл» выше). По паре ГО → ДО таблицы гудвилла с `use_goodwill` при + `goodwill_enabled` версии: исключения ГО `1170` на ФВ (все строки 58.01 и потомков пары, как в таблице гудвилла), + ДО `1310` на P (без каскада); G — в строку `GOODWILL`; агрегат правила 2 той же пары (компания 1 = ГО, компания 2 = + ДО), если он есть, **не применяется** — ни автосумма, ни корректировка (применённая корректировка такого агрегата + в сводку корректировок п. 11 не входит, предупреждение `CORRECTION_SUPERSEDED_BY_GOODWILL`). Строка 58.01 пары перенесена аналитиком в + другое правило или отмечена «не учитывать» — она всё равно в ФВ, предупреждение `GOODWILL_ROW_REASSIGNED` (ВГО по ней + может исключиться дважды, решает аналитик). Гудвилл включён, а применимых пар нет — `GOODWILL_NOT_APPLIED`. + Строка `GOODWILL` — раздел I, сразу после 1170, без разреза по компаниям; `goodwill_adjust = bank_amount = Σ G` + применимых пар, до целых тысяч; входит в 1100 и 1600; `kfo_result.goodwill_total` — то же значение. Место строки — + по DOC-259025 (отдельная строка раздела I); макет `[31-04]` показывает «В т.ч. Гудвилл» под 1170 — OQ-185. +9. **Расхождения по статьям.** По всем агрегатам режима `min` (ББ и ОФР), **в том числе односторонним** (тогда + расхождение — вся непустая сторона) и с отрицательной стороной: `|amount_1 − amount_2|` — на статью стороны с + большей суммой (компания 1 — `article_1_line`, компания 2 — `article_2_line`), независимо от корректировок (OQ-186). + Агрегаты `manual`, `min_or_full` и гудвилл-пары в расхождения не входят. По строке — Σ в рублях → тыс. руб. до целых + (`deviation_amount`). `total_deviation_bb` / `total_deviation_ofr` — Σ по строкам бланка своего отчёта, без итогов; + `has_deviations` — хотя бы одна строка ≠ 0. +10. **Контрольные пороги** (ТЗ 6.8, Методика пп. 5.1.2.4, 5.4; величины — КФО банка, тыс. руб.): + - ББ: ЧА = строка 1300 (как FNR03, OQ-187), ВБ = 1600. `ЧА ≥ 10 % ВБ` → база ЧА, предел 10 % ЧА; иначе (в том + числе ЧА ≤ 0) → база ВБ, предел 1 % ВБ. + - ОФР: `2200 ≥ 0` и `2200 / 2110 ≥ 10 %` → база 2200, предел 10 % 2200; иначе → база 2110, предел 1 % 2110. + `2110 = 0` → база 2110, предел 0 (OQ-188). + - Превышение — `total_deviation > предел`; равно — норма. Процент от базы — для сводки, `ROUND_HALF_UP` до двух + знаков, база = 0 → `null`. Предел — `ROUND_HALF_UP` до двух знаков. + - Превышение хотя бы по одному отчёту → нужно обоснование принятия риска (`threshold_justification`). Обязательность + перед отправкой на верификацию (OQ-189, вариант черновика 2) проверяет переход статуса — AURORA-2002 / 1414. +11. **Реквизиты формы** (ТЗ 6.4, DOC-274636 правило 9): ГК, ГСК, отчётная дата, номер и статус версии, сценарий, + подход (`approach` — пакет документов), гудвилл учтён да/нет и сумма, расхождения ББ/ОФР и превышения, обоснование, + корректировки — число агрегатов с применённой корректировкой и Σ их исключений по отчёту, обеих сторон + (`kfo_result.corrections_count_bb/_ofr`; `corrections_amount_bb/_ofr` = Σ `eliminated_manual` строк бланка без + итогов, тыс. руб.), причины отклонений от Методики + (`consolidation_version.methodology_deviations` — ввод на вкладке «КФО»), решение УОБ (номер и дата — только + чтение). «Подход по Методике» и «согласовано клиентом» — не делаем до ответа заказчика (OQ-190, OQ-191). +12. **Пересчёт и заморозка.** Результат хранится у версии (`kfo_result`, одна запись). Пока версия не заморожена, + чтение отдаёт результат по текущим данным: если входы изменились после `computed_at`, сервер пересчитывает и + сохраняет до ответа (как обнаруживать изменения — AURORA-1417). Кнопки «Рассчитать» нет (OQ-192). Комментарии строк + и обоснование переносятся при пересчёте по (тип отчёта, код строки). При «Верифицирован» `is_frozen = true` (ставит + AURORA-2002) — дальше результат только читается. **Предупреждения** `{code, message, legal_entity_id?, + statement_type?, count?}` не блокируют расчёт; они и сводка корректировок — часть результата и **хранятся в + `kfo_result`** (`warnings`), чтобы замороженная версия отдавала то, что видели при верификации. Коды: + `FORM_MISSING`, `FORM_AMBIGUOUS`, `FORM_UNITS_UNKNOWN`, `LINE_OUTSIDE_BLANK`, `UNALLOCATED_ROWS`, + `INCOME_RULES_MISSING`, `GOODWILL_NOT_APPLIED`, `GOODWILL_ROW_REASSIGNED`, `CORRECTION_SUPERSEDED_BY_GOODWILL`. + Правка ручных полей — аудит `KFO_EDIT`. + +**Пример** (тыс. руб., если не сказано иное; те же числа — в `examples` контракта). Три компании: А «Альфа», Б «Бета», +В «Гамма»; отчётная дата 01.10.2025 (ФСБУ 4). Агрегаты, рубли: + +| Агрегат | `amount_1` | `amount_2` | Что исключается | +|---|---|---|---| +| пр. 6, А → Б | 102 000 000,00 | 100 000 400,50 | отклонение 2,00 % (1,9996) — автосумма 100 000 400,50: А 1230, Б 1520 | +| пр. 6, Б → А | 10 000 000,00 | 50 000 000,00 | 400 % — автосумма 0; корректировка Дт Б 1230 10 000 000, Кт А 1520 10 000 000 | +| пр. 6, А → В | 30 000 000,00 | 29 100 000,00 | 3,09 % (автосумма была бы 29 100 000), но корректировка: Дт А 1230 20 000 000 + А 1170 5 000 000, Кт В 1520 22 500 000 — итоги 25 000 000 и 22 500 000, каждая сторона исключает 22 500 000: А 1230 20 000 000, А 1170 2 500 000, В 1520 22 500 000 | +| пр. 2, А → Б | 50 000 000,00 | 40 000 000,00 | гудвилл-пара (ФВ 50 000 000, УК 40 000 000, d = 80 %, P = 32 000 000, G = 18 000 000): А 1170 50 000 000, Б 1310 32 000 000, `GOODWILL` 18 000; агрегат не применяется | +| пр. 2, А → В | 15 000 000,00 | 0,00 | `min_or_full`, стороны 2 нет → 15 000 000: А 1170; у В каскад 1310 (есть 10 000 000) → 10 000 000, 1350 (3 000 000) → 3 000 000, остаток 2 000 000 → 1370 | +| ОФР, А → Б | 10 000 000,00 | 10 000 000,00 | по правилам ОФР 1–2 (S без НДС): А 2110, Б 2120 | + +ББ (по компании: по данным клиента / в т. ч. взаимные / очищенные): + +| Строка | А | Б | В | КФО банка | Расхождения | +|---|---|---|---|---|---| +| 1170 | 70 000 / 67 500 / 2 500 | 0 / 0 / 0 | 0 / 0 / 0 | 2 500 | 0 | +| `GOODWILL` | — | — | — | 18 000 | 0 | +| 1100 | 70 000 / 67 500 / 2 500 | 0 / 0 / 0 | 0 / 0 / 0 | 20 500 (2 500 + гудвилл 18 000) | — | +| 1230 | 520 000 / 120 000 / 400 000 (70,42 %) | 130 000 / 10 000 / 120 000 (21,13 %) | 48 000 / 0 / 48 000 (8,45 %) | 568 000 | 2 900 | +| 1600 | 590 000 / 187 500 / 402 500 | 130 000 / 10 000 / 120 000 | 48 000 / 0 / 48 000 | 588 500 | — | +| 1310 | 100 000 / 0 / 100 000 | 40 000 / 32 000 / 8 000 | 10 000 / 10 000 / 0 | 108 000 | 0 | +| 1350 | 0 / 0 / 0 | 0 / 0 / 0 | 3 000 / 3 000 / 0 | 0 | 0 | +| 1370 | 280 000 / 0 / 280 000 | −20 000 / 0 / −20 000 | 5 000 / 2 000 / 3 000 | 263 000 | 0 | +| 1300 | 380 000 / 0 / 380 000 | 20 000 / 32 000 / −12 000 | 18 000 / 15 000 / 3 000 | 371 000 | — | +| 1410 | 50 000 / 0 / 50 000 | 0 / 0 / 0 | 0 / 0 / 0 | 50 000 | 0 | +| 1520 | 160 000 / 10 000 / 150 000 | 110 000 / 100 000 / 10 000 | 30 000 / 22 500 / 7 500 | 167 500 | 40 000 | +| 1700 | 590 000 / 10 000 / 580 000 | 130 000 / 132 000 / −2 000 | 48 000 / 37 500 / 10 500 | 588 500 | — | + +- 1230 А: 100 000 400,50 + 20 000 000 = 120 000 400,50 руб. → 120 000 тыс. (округление ячейки); Б 1520 — + 100 000 400,50 → 100 000. Корректировка Б → А заменила автосумму (0), а не прибавилась к ней; корректировка А → В — + тоже, хотя автосумма была бы 29 100 000. +- Расхождения: 1 999 599,50 (А → Б) + 900 000 (А → В) = 2 899 599,50 руб. → 2 900 на 1230 А (сторона 1 больше); + 40 000 000 → 40 000 на 1520 А (сторона 2 Б → А больше). Пары правила 2 в расхождения не входят. Всего ББ 42 900. +- Порог ББ: ЧА 371 000 ≥ 10 % ВБ (58 850) → база ЧА, предел 37 100; 42 900 > 37 100 — **превышение** (11,56 % от ЧА), + нужно обоснование. +- Сходимость: 1600 = 1700 = 588 500 (у ГО −ФВ, у ДО −P, гудвилл +G). +- **Гудвилл выключен** (та же версия, `goodwill_enabled = false`): агрегат пр. 2 А → Б — `min_or_full` → 40 000 000; + А 1170 −40 000 (всего 57 500), Б 1310 −40 000 (каскад уместился в 1310); `GOODWILL` 0; 1600 = 1700 = 580 500; + ЧА 363 000, предел 36 300, превышение 11,82 %. + +ОФР (у Б 2120 в форме «без знака» +60 000 → приведено к −60 000; у В формы ОФР нет — `FORM_MISSING`, нули): + +| Строка | А | Б | В | КФО банка | +|---|---|---|---|---| +| 2110 | 200 000 / 10 000 / 190 000 | 80 000 / 0 / 80 000 | 0 / 0 / 0 | 270 000 | +| 2120 | −120 000 / 0 / −120 000 | −60 000 / −10 000 / −50 000 | 0 / 0 / 0 | −170 000 | +| 2100 | 80 000 / 10 000 / 70 000 | 20 000 / −10 000 / 30 000 | 0 / 0 / 0 | 100 000 | +| 2200 | 50 000 / 10 000 / 40 000 | 15 000 / −10 000 / 25 000 | 0 / 0 / 0 | 65 000 | + +Σ `vgo_amount` строки 2100 = 10 000 − 10 000 = 0 — валовая прибыль группы не меняется. Порог ОФР: 65 000 / 270 000 = +24 % ≥ 10 % → база 2200, предел 6 500; расхождений ОФР нет (агрегаты ОФР 1–2 — не `min`). + +**Прогон на «Русском бисквите»** (критерий AURORA-1546): версия на 01.10.2024 — бланк 66н (есть 1120, 1231, 2930; +нет 1105, 1215, 2420), против листа `01.10.2024` эталона `Консолидация_Группа РБ.xlsx` (колонки «значение на дату | +в т. ч. взаимные обязательства | очищенные показатели | доля в показателе по группе»). Ожидаемые расхождения с эталоном: +алгоритм по ТЗ (min, порог 5 %), а эталон исключает каждую сторону полностью (OQ-155); ручные правки аналитика — +показываются, не прячутся. **Сравнение с КФО клиента** (`scenario = CONFIRM`): `client_kfo_line` построчно: `client_amount`, `client_vgo_amount` (ручной ввод), `bank_amount`, `bank_vgo_amount` (из `kfo_result_line`), `deviation_percent` к наименьшему, причина. diff --git a/context/07-open-questions.md b/context/07-open-questions.md index b8cdb4c..35c42cc 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -183,3 +183,18 @@ | OQ-175 | Процент расхождения шахматки при отрицательной меньшей стороне: `variance_pct = variance / min(amount_a, amount_b) * 100` при отрицательном сальдо (возможно по §1) уходит в минус — пара (100, −5) даёт −2100 %, порог > 5 % не срабатывает, пара не подсвечивается. Считать от `\|min\|`, от меньшей по модулю стороны или подсвечивать пары с разными знаками всегда? | ревью arm-core #165 (AURORA-1651), 22.10.2026 | §6, шахматка, xlsx | открыт; до решения — как в коде (min со знаком); у пар ВГО §5 суммы сторон тоже со знаком (решение владельца 23.10.2026), но при `min ≤ 0` процент не считается и тройка подсвечивается | | OQ-176 | Счёт `000` «Вспомогательный» (технический счёт 1С для ввода остатков, `kind = AP`, `is_off_balance = false`) входит в шахматку «все счета». Исключать его вместе с забалансовыми? | ревью arm-core #165 (AURORA-1651), 22.10.2026 | §6, режим «все счета» | открыт; до решения — входит (решение владельца 22.10.2026: не трогать) | | OQ-178 | Порог превышения отклонения агрегата в таблице корректировок — ровно 5 % в допуске (строго `>`) или на проверку («от 5 % включительно», как читается DOC-272613/ТЗ)? Аналитик (Лысенко) на прямой вопрос не отвечал | `dev/api-contract-corrections.md` §2; R-34/R-35 (`arm-core`, `openspec/changes/corrections-table-ui/design.md`) | §5 п. 2a, §8 «Корректировка», экран «Таблица корректировок» | открыт; реализовано строго `>` 5 % (R-35, `domain/corrections.py`) — фронт и сервер синхронны между собой, но не точно со спорным текстом ТЗ; правка при подтверждении — один оператор в `editorModel.ts`/`CorrectionTable.tsx` и в `domain/corrections.py` | +| OQ-180 | Строки КФО вне бланка ФСБУ: нужны ли строки FNR03 «ППА», «Право пользования активами», «Износ основных средств», «Расхождение баланса по Активу и Пассиву», «Форма 0710002 с.2», «Дополнительная информация»? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 1; FNR03 (DOC-221276) | §8 «КФО» п. 1 | открыт, вопрос заказчику; допущение принято (docs #206): только бланк редакции на отчётную дату + строка `GOODWILL` | +| OQ-181 | Состав колонок формы КФО: колонки макета `[31-04]` плюс расчётные «Расхождения» или ещё «КФО клиента» и ручные «Расхождения/Примечания», как в FNR03 и Прил. 10? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 2; макет `[31-04]`; ТЗ Прил. 10 | §8 «КФО» п. 7; `dev/api-contract-kfo.md` | открыт, вопрос заказчику; допущение принято (docs #206): колонки макета + «Расхождения» (расчётные); «Примечания» = «Комментарий»; КФО клиента — только на «Подтверждении КФО клиента» | +| OQ-182 | Значение компании без подтверждённой формы ББ/ОФР: 0 с предупреждением, строки из ОСВ по счетам статьи (нужна таблица «строка ↔ счета») или КФО не считается? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 3; ТЗ Прил. 10 («Баланс (форма 01)» / «ОСВ»); `kfo_result_line.source_kind` | §8 «КФО» п. 2 | открыт, вопрос заказчику; допущение принято (docs #206): 0 и `FORM_MISSING`; `source_kind` всегда `balance` | +| OQ-183 | Итоговая строка КФО (1100…1700, 2100…2500): по формуле бланка из очищенных строк или как итог формы клиента минус взаимные слагаемых? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 7 | §8 «КФО» п. 7 | открыт, вопрос заказчику; допущение принято (docs #206): итог формы клиента минус Σ взаимных слагаемых (совпадает с подписанным клиентом) | +| OQ-184 | Округление рублей до тысяч: ячейка «в т. ч. взаимные» (компания × строка) после сложения, каждая тройка до сложения, без округления (тысячи с копейками) или банковское? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 8 | §8 «КФО» п. 6 | открыт, вопрос заказчику; допущение принято (docs #206): ячейка после сложения в рублях, до целых тысяч, половина от нуля | +| OQ-185 | Место гудвилла в КФО: отдельная строка раздела I (DOC-259025, FNR03 «Заполнение поля Гудвил»), «В т.ч. Гудвилл» под 1170 (макет `[31-04]`) или строка 1105 бланка ФСБУ 4 (см. OQ-37)? | DOC-259025; DOC-274636, вопрос 9; макет `[31-04]`; ФСБУ 4/2023 | §8 «КФО» п. 8 | открыт, вопрос заказчику; допущение принято (docs #206): служебная строка `GOODWILL` раздела I после 1170, входит в 1100 и 1600, без разреза по компаниям; решают методологи | +| OQ-186 | «Совокупный объём расхождений» для порога: разницы сторон всех пар как найдены, только пары без корректировки или остатки после корректировок? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 10; FNR03 шаг 4 | §8 «КФО» п. 9 | открыт, вопрос заказчику; допущение принято (docs #206): разницы сторон всех агрегатов `min` как найдены, независимо от корректировок | +| OQ-187 | ЧА для порога ББ: строка 1300 КФО банка (FNR03) или расчёт по приказу Минфина № 84н (≈ 1300 + 1530)? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 11; ТЗ 6.8; Методика 5.1.2.4 | §8 «КФО» п. 10 | открыт, вопрос заказчику; допущение принято (docs #206): строка 1300 | +| OQ-188 | Порог при нулевой базе (2110 = 0 или 1600 = 0): предел 0 (любое расхождение — превышение) или порог не применяется? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», допущение 12 | §8 «КФО» п. 10 | открыт, вопрос заказчику; допущение принято (docs #206): предел 0 | +| OQ-189 | Что делает превышение порога: только подсветка, обязательное обоснование до отправки на верификацию, только до финала или блокировка верификации? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 13; DOC-037736 («НЕ ТОЧНО»); Excel В-01 | §8 «КФО» п. 10; AURORA-2002/1414 | открыт, вопрос заказчику; допущение принято (docs #206): обоснование обязательно до отправки на «Верификацию РФ/ГО» и до финала; проверяет переход статуса | +| OQ-190 | «Подход» в реквизитах КФО: пакет документов (`approach` BASIC/EXTENDED, ТЗ 5.1) или подход по Методике (сноска 15: «по Методике / индивидуальный / принята аудированная отчётность»)? Для второго нужно новое поле версии | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 14 | §8 «КФО» п. 11 | открыт, вопрос заказчику; допущение принято (docs #206): показывается только `approach`; поле «подход по Методике» не заводим до ответа | +| OQ-191 | Где фиксируется согласование КФО клиентом (Методика 5.8, 6.1): поле версии «да / нет / не направлялось» с датой или не вести в АРМ? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 15 | §8 «КФО» п. 11 | открыт, вопрос заказчику; допущение принято (docs #206): не ведём до ответа (вариант черновика — поле версии) | +| OQ-192 | Пересчёт КФО: автоматически после каждого изменения (Excel Р-11, Р-23), по кнопке «Рассчитать КФО» с пометкой «устарела» (FNR03) или только при открытии? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 16; AURORA-1417 | §8 «КФО» п. 12 | открыт, вопрос заказчику; допущение принято (docs #206): автоматически: чтение пересчитывает устаревший результат, кнопки нет | +| OQ-193 | Правило ББ 1 (76.7.1 / 76.7.9) автоматически по DOC-274230: обе суммы — у компании 2 (Кт 76.7.1 − Дт 76.7.9, каскад 1150 → 1450 → 1550), а сторона агрегата §5 устроена «компания 1 — сторона 1». Как переложить правило в seed (стороны, режим)? | DOC-274230 правило 1; `arm-core/api/seed/core/elimination_rules_v1.yaml` | §5 п. 1, §8 «КФО» п. 4 | открыт, задача команды (follow-up AURORA-1546); до решения правило 1 — `manual`, только корректировкой | +| OQ-194 | КФО при неполном комплекте документов (обязательный комплект и проверки перед расчётом отложены, Excel В-01, В-08): считать по тому, что подтверждено и привязано, с предупреждением, или не считать, пока комплект не полон? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», допущение 17 | §8 «КФО» п. 2 | открыт, вопрос заказчику; допущение принято (docs #206): считать, у компании без формы — нули и `FORM_MISSING` | diff --git a/context/08-source-docs-index.md b/context/08-source-docs-index.md index 5838570..303ad4b 100644 --- a/context/08-source-docs-index.md +++ b/context/08-source-docs-index.md @@ -48,8 +48,8 @@ | Файл | Что | |---|---| -| `data-model-v5.24.dbml` | источник истины; changelog v5.1…v5.24 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.23 остаются рядом как история) | -| `entities-v5.24.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.23 остаются как история) | +| `data-model-v5.26.dbml` | источник истины; changelog v5.1…v5.26 в шапке (v5.25 — PR #203); блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.24 остаются рядом как история) | +| `entities-v5.26.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.24 остаются как история) | | `diagrams/part01…09.{svg,png}` | диаграммы v5.21 по частям; перерисовываются `make diagrams` (`scripts/gen_model_diagrams.py`) | | `source/tz/data-model-part1_4.dbml` (лежит не в `model/`, а в экспорте ТЗ) | v4 часть 1 — историческое | diff --git a/context/KICKOFF-PROMPT.md b/context/KICKOFF-PROMPT.md index cdb9997..1a44495 100644 --- a/context/KICKOFF-PROMPT.md +++ b/context/KICKOFF-PROMPT.md @@ -9,8 +9,8 @@ 1. `CLAUDE.md` (workspace) и `docs/context/00-workspace-layout.md` — как устроена рабочая область, статус аналитики, макет. 2. `docs/context/01-project-overview.md`, `02-glossary.md` — предметная область. -3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.24.dbml` и - `docs/model/entities-v5.24.md`. +3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.26.dbml` и + `docs/model/entities-v5.26.md`. 4. `docs/context/05-domain-rules.md` — алгоритмы (особенно §1.0 и §12 — парсеры ищут структуру по содержимому); `06-test-data.md`; `07-open-questions.md`; `10-user-journey.md`; `09-delivery-plan.md`. 5. `arm-core/`, `tabular-parser/`, `scan-gateway/` — три репозитория, развёрнутые из нулевого коммита шаблона команды, @@ -37,7 +37,7 @@ NATS, MinIO, эмулятор ИС Сканирование), контракт ` `07-open-questions.md`; если нужен ответ для среза — делай допущение по правилам из этого файла. Ограничения на всё время работы: -- имена таблиц, колонок и enum'ов — ровно как в DBML v5.24; поля, которых нет в DBML, не изобретать; +- имена таблиц, колонок и enum'ов — ровно как в DBML v5.26; поля, которых нет в DBML, не изобретать; - алгоритмы — только из `05-domain-rules.md`; из страниц ТЗ напрямую не кодировать; - парсеры: никаких координат ячеек, букв колонок, имён листов и номеров строк в коде — структура по содержимому, синонимы в `rules/*.yaml`; фикстуры — регрессия, не спецификация; diff --git a/dev/api-contract-corrections.md b/dev/api-contract-corrections.md index 8a346f5..8223d91 100644 --- a/dev/api-contract-corrections.md +++ b/dev/api-contract-corrections.md @@ -174,9 +174,9 @@ side_1 AggregateSideOut дебетовая сторона ( side_2 AggregateSideOut кредитовая сторона (компания 2) deviation_amount money deviation_percent percent | null к меньшей стороне; null — меньшая сторона ≤ 0 -exceeds_threshold bool deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой +exceeds_threshold bool deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой; у min_or_full — всегда false status "AUTO" | "DISCREPANCY" | "ONE_SIDED" | "MANUAL" как EliminationTotalItemOut -eliminated_amount money автоматическая сумма элиминации агрегата (§5), справочно +eliminated_amount money автосумма агрегата по режиму правила (§8 «КФО» п. 4) — её исключает КФО, если нет корректировки composition_changed bool состав или суммы изменились после применённой корректировки (ответ 5; ответ п. 10 — подсвечивать) correction CorrectionOut | null действующая корректировка (APPLIED; DRAFT — зарезервирован) ``` @@ -257,7 +257,12 @@ rule_id? (uuid, для ASSIGN)}`. агрегаты и нераспределённые строки (с `line_code` и `candidate_rule_ids`). Нынешняя упаковка считает каждое правило отдельно (TODO в `intercompany_matrix.build_pairs`) — для таблицы корректировок строка должна попадать не более чем в одно правило. -- Правила ББ 1 и 2 (DOC-274230) имеют свой расчёт и не наследуют min; что идёт в `eliminated_amount` по ним — по DOC-274230. +- Правила ББ 1 и 2 (DOC-274230) имеют свой расчёт и не наследуют min. **Правило 2** — режим `min_or_full` (DBML v5.26, + `05-domain-rules.md` §8 «КФО» п. 4; seed и `services/correction_source.py` правятся в AURORA-1546): обе стороны > 0 → + `eliminated_amount = min`, `status = AUTO`; сторона 2 пуста → `eliminated_amount = amount_1`, `status = ONE_SIDED`; + сторона 1 пуста → 0, `ONE_SIDED`; `exceeds_threshold = false` всегда (порога 5 % у правила нет). **Правило 1** остаётся + `manual` (`status = MANUAL`, 0) до переработки seed (OQ-193). Автосумма — **та же функция, по которой считает КФО**: + что аналитик видит в `eliminated_amount`, то и исключает КФО, если корректировки нет (корректировка её заменяет). - ОФР: тот же ответ с `statement_type = income` **(ждёт ответа в AURORA-1396)**; до него сервер может отдавать пустые `aggregates` и `unallocated` для `income`. diff --git a/dev/api-contract-corrections.openapi.yaml b/dev/api-contract-corrections.openapi.yaml index 31f890d..997632f 100644 --- a/dev/api-contract-corrections.openapi.yaml +++ b/dev/api-contract-corrections.openapi.yaml @@ -282,9 +282,9 @@ components: deviation_percent: { $ref: '#/components/schemas/Percent' } exceeds_threshold: type: boolean - description: deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой + description: deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой; у правила min_or_full — всегда false status: { type: string, enum: [AUTO, DISCREPANCY, ONE_SIDED, MANUAL] } - eliminated_amount: { $ref: '#/components/schemas/Money' } + eliminated_amount: { $ref: '#/components/schemas/Money', description: 'Автосумма агрегата по elimination_mode (min / min_or_full / manual; 05-domain-rules.md §8 «КФО» п. 4); её исключает КФО, если нет корректировки' } composition_changed: { type: boolean, description: Состав или суммы изменились после применённой корректировки } correction: oneOf: diff --git a/dev/api-contract-kfo.md b/dev/api-contract-kfo.md new file mode 100644 index 0000000..1a75aa7 --- /dev/null +++ b/dev/api-contract-kfo.md @@ -0,0 +1,181 @@ +# Контракт API результата КФО (AURORA-1546 / AURORA-1547) + +Контракт вкладки «КФО» отчёта версии консолидации — **до реализации** (docs #206, OpenSpec change +`kfo-calculation-contract`). Машиночитаемая копия — `dev/api-contract-kfo.openapi.yaml` с тремя полными примерами +ответа (их можно брать как фейк для экрана); она генерируется `scripts/kfo_contract/gen_kfo_openapi.py` из +сквозного примера §8 (`scripts/kfo_contract/kfo_example.py`, там же проверки правил) — руками не править. Когда расчёт появится в `arm-core`, копия выгружается из `app.openapi()` и +сверяется тестом, как у гудвилла; если файлы разойдутся с кодом — прав код, оба файла правятся вслед за ним. + +Источники, в порядке приоритета: +1. `05-domain-rules.md` §8 «КФО» — алгоритм (п. 1–12) и сквозной пример. Здесь формулы не повторяются — только + ссылки «п. N». +2. DBML v5.26: `kfo_result`, `kfo_result_line`, `kfo_result_line_company`, `consolidation_version`. +3. DOC-274647 «Экран: КФО» (черновик) и макет `[31-04]`, `[31-04-01]` — состав колонок, состояния экрана. + +## 1. Общее + +Как в `dev/api-contract-corrections.md` §1 и `dev/api-contract-goodwill.md` §1: +- префикс `/api/v1`, `Authorization: Bearer `; +- **чтение — право `KFO_VIEW`** (не `CONSOLIDATION_VIEW`, как у корректировок и гудвилла: право в seed есть и означает + ровно это), **правки — `CONSOLIDATION_EDIT`** (`KFO_EDIT` в seed нет); +- версия вне видимости — 404 `NOT_FOUND`, как несуществующая; +- доменные ошибки — `{"detail": {"code", "message", ...}}`, 422 валидации тела — массив pydantic; +- **деньги — строки с двумя знаками в тыс. руб.** (`"588500.00"`), поле `units = "thousand_rub"`. Это отличие от + корректировок и гудвилла, где рубли: результат КФО по определению в тысячах (округление — часть расчёта, §8 «КФО» + п. 6), рубли из него не восстановить. Переключатели «Единицы» (тыс. / млн) и «Знаков после запятой» — только показ; +- проценты — строки с двумя знаками (`"70.42"`), `null` — не определён (нулевая база); +- порядок проверок правок: право (403) → видимость (404) → версия редактируема (409 `VERSION_NOT_EDITABLE`, + `LOCK_REQUIRED`, `LOCKED_BY_OTHER`) → операция. Фронт берёт `editable` / `read_only_reason` из ответа и правило сам не + повторяет; у пользователя без `CONSOLIDATION_EDIT` — `read_only_reason = "VERSION_NOT_EDITABLE"`. + +**Фронт ничего не считает.** Доли, итоги, «в т. ч. взаимные», расхождения, базы и пределы порогов, признак превышения +и подсветка строки базы приходят с сервера. + +## 2. Понятия + +- **Строки** — бланк ББ и ОФР редакции на отчётную дату версии (66н до 31.12.2024, ФСБУ 4 позже) плюс служебная + `GOODWILL` в разделе I сразу после 1170 (§8 «КФО» п. 1, 8). Приходят все строки бланка в его порядке (`ordinal`), + включая нулевые; `is_total` — итоговая строка. +- **По компании** (`companies[]` строки, порядок — как `KfoResultOut.companies`): «По данным клиента» + (`client_amount`), «В том числе взаимные обязательства / обороты» (`vgo_amount`), «Очищенные показатели» + (`cleaned_amount = client_amount − vgo_amount`), «Доля в показателе по группе, %» (`share_percent`; `null` при нулевом + итоге строки или `|доля| ≥ 1000 %`). Строки расходов + (2120, 2210, 2220, 2330, 2350) — со знаком минус, и их `vgo_amount` тоже (п. 2, 6). У `GOODWILL` разреза нет. +- **По строке:** `sum_amount` (Σ клиента), `vgo_amount` (Σ взаимных = `eliminated_auto + eliminated_manual`), + `goodwill_adjust` (у `GOODWILL`, 1100, 1600), **`bank_amount`** — «КФО, подготовленная банком» + (`sum_amount − vgo_amount + goodwill_adjust`), `deviation_amount` — «Расхождения» (у итогов `null`), `comment`. +- **Сводка расхождений** (`summary.balance`, `summary.income`) — две плашки экрана: сумма расхождений, база (`base_kind`: + ЧА / ВБ / прибыль от продаж / выручка), предел и процент от базы, `exceeded` — красная плашка (п. 10). При превышении + у строки базы `is_threshold_base = true` — подсветка. +- **Обоснование** (`threshold_justification`) — принятие риска при превышении; `justification_required` — превышение + есть хотя бы по одному отчёту. Обязательность перед отправкой на верификацию проверяет переход статуса (AURORA-2002), + не этот API. +- **Реквизиты формы** (`requisites`, п. 11): ГК, ГСК, отчётная дата, номер и статус версии, сценарий, подход (пакет + документов), причины отклонений от Методики (`methodology_deviations` — редактируется здесь), решение УОБ (только + чтение), корректировки — число агрегатов с применённой корректировкой и Σ их исключений. +- **Предупреждения** (`warnings[]`, п. 12) — не блокируют: `FORM_MISSING`, `FORM_AMBIGUOUS`, `FORM_UNITS_UNKNOWN`, + `LINE_OUTSIDE_BLANK`, `UNALLOCATED_ROWS` (`count` — строк), `INCOME_RULES_MISSING`, `GOODWILL_NOT_APPLIED`, + `GOODWILL_ROW_REASSIGNED`, `CORRECTION_SUPERSEDED_BY_GOODWILL` (корректировка агрегата правила 2 не применена — + пару закрыл гудвилл). Тексты `message` — с сервера, фронт показывает как есть. + +## 3. Эндпоинты + +| Метод и путь | Право | Запрос | Ответ | Ошибки | +|---|---|---|---|---| +| `GET /consolidations/versions/{version_id}/kfo` | `KFO_VIEW` | — | 200 `KfoResultOut` | 404 `NOT_FOUND` | +| `PUT /consolidations/versions/{version_id}/kfo/annotations` | `CONSOLIDATION_EDIT` | `KfoAnnotationsIn` | 200 `KfoResultOut` | 404 `NOT_FOUND`, 409 правок версии, 422 `UNKNOWN_LINE`, 422 валидации тела | + +Выгрузки («Таблица», «Форма 2.03.01.01с») — AURORA-1565, отдельным контрактом. + +### 3.1 `GET …/kfo` — результат + +Весь результат одним ответом, без страниц. **Пока версия не заморожена, ответ — по текущим данным версии**: если +что-то изменилось после `computed_at` (документы, сопоставление, разнесение, корректировки, гудвилл), сервер +пересчитывает и сохраняет результат до ответа (как он это обнаруживает — AURORA-1417). Кнопки «Рассчитать» нет. У +замороженной версии (`is_frozen = true`, ставится при «Верифицирован») — сохранённый результат без пересчёта, +`editable = false`. + +Версия без компаний или без форм — не ошибка: строки бланка с нулями и `warnings` (`FORM_MISSING` по каждой компании +и отчёту). + +### 3.2 `PUT …/kfo/annotations` — ручные поля + +Тело: + +```json +{ + "comments": [{"statement_type": "balance", "line_code": "1520", "comment": "Акт сверки на 01.10 не получен"}], + "threshold_justification": "Расхождение по 1520 — сверка в работе; риск принят", + "methodology_deviations": null +} +``` + +- Поле, которого нет в теле, не меняется; `null` — очищает. Пустой объект `{}` — допустим, ничего не пишет. +- `comments[]` — только изменённые строки; `comment` ≤ 2000 символов, `null` или пустой после обрезки — очистить. + Строки нет в бланке версии (или `GOODWILL` в `income`) — 422 `UNKNOWN_LINE` с `detail.statement_type`, + `detail.line_code`; ничего не сохраняется. +- `threshold_justification`, `methodology_deviations` — ≤ 4000 символов; длиннее — 422 массивом pydantic. +- Обоснование можно сохранить и без превышения — сервер его не стирает; `justification_required` от этого не меняется. +- Всё одной транзакцией; комментарии и обоснование переживают пересчёт (по `statement_type`, `line_code`). +- Аудит — одно событие `KFO_EDIT` на запрос (`object_type = kfo_result`, раздел `KFO`), `before` / `after` — только + изменённые поля. Запрос без изменений аудит не пишет. + +Ответ — `KfoResultOut` целиком (с пересчётом, если он нужен). + +## 4. Схемы + +Полностью — в OpenAPI. Кратко: + +``` +KfoResultOut + version_id uuid; computed_at date-time; is_frozen bool; editable bool; read_only_reason enum|null + units "thousand_rub" + companies[] {legal_entity_id, name, inn} — порядок колонок (по имени) + requisites KfoRequisitesOut + summary {balance: KfoThresholdOut, income: KfoThresholdOut} + justification_required bool; threshold_justification string|null + goodwill {enabled bool, total money} + balance, income {lines: KfoLineOut[]} + warnings[] KfoWarningOut {code, message, legal_entity_id?, statement_type?, count?} + +KfoLineOut + line_code, line_name, ordinal, is_total, is_threshold_base + companies[] {legal_entity_id, client_amount, vgo_amount, cleaned_amount, share_percent|null} (у GOODWILL — []) + sum_amount, vgo_amount, eliminated_auto, eliminated_manual, goodwill_adjust, bank_amount money + deviation_amount money|null (у итогов null) + comment string|null + +KfoThresholdOut + deviation_total money; base_kind NET_ASSETS|BALANCE_TOTAL|SALES_PROFIT|REVENUE; base_amount money + limit_percent "10.00"|"1.00"; limit_amount money; deviation_percent pct|null; exceeded bool + +KfoRequisitesOut + company_group_name, related_group_name|null, reporting_date, version_no, version_status, scenario, approach + methodology_deviations|null, uob_decision_no|null, uob_decision_date|null + corrections_balance, corrections_income {count int, amount money} + +KfoAnnotationsIn {comments?: [{statement_type, line_code, comment|null}], threshold_justification?, methodology_deviations?} +``` + +## 5. Состояния экрана (DOC-274647) → поля ответа + +| Состояние | Как видно из ответа | +|---|---| +| Нет форм у части компаний | `warnings[].code = FORM_MISSING` с `legal_entity_id`, `statement_type`; у компании нули | +| ОФР не исключается автоматически | `INCOME_RULES_MISSING` (до AURORA-1999); взаимные обороты — только корректировками | +| Есть нераспределённые строки | `UNALLOCATED_ROWS`, `count` | +| Превышение порога | `summary.*.exceeded = true`, `is_threshold_base` у строки базы, `justification_required = true` | +| Гудвилл учтён | `goodwill.enabled = true`, `goodwill.total`, строка `GOODWILL` с `bank_amount` | +| Только чтение | `editable = false`, `read_only_reason`; при «Верифицирован» ещё `is_frozen = true` | + +## 6. Примеры + +Три полных ответа — в OpenAPI (`examples` у `GET …/kfo`), на числах сквозного примера §8 «КФО» (три компании, бланк +сокращён до строк примера; настоящий ответ содержит все строки бланка): +- `goodwill_exceeded` — гудвилл включён, превышение порога ББ (42 900 > 37 100), `justification_required = true`; +- `goodwill_disabled` — та же версия без учёта гудвилла: строка `GOODWILL` = 0, 1600 = 580 500; +- `frozen` — «Верифицирован»: `is_frozen`, `editable = false`, обоснование, комментарий 1520, реквизиты УОБ. + +Фрагмент `goodwill_exceeded` — строка 1230: + +```json +{ + "line_code": "1230", "line_name": "Дебиторская задолженность", "ordinal": 40, + "is_total": false, "is_threshold_base": false, + "companies": [ + {"legal_entity_id": "1111…", "client_amount": "520000.00", "vgo_amount": "120000.00", "cleaned_amount": "400000.00", "share_percent": "70.42"}, + {"legal_entity_id": "2222…", "client_amount": "130000.00", "vgo_amount": "10000.00", "cleaned_amount": "120000.00", "share_percent": "21.13"}, + {"legal_entity_id": "3333…", "client_amount": "48000.00", "vgo_amount": "0.00", "cleaned_amount": "48000.00", "share_percent": "8.45"} + ], + "sum_amount": "698000.00", "vgo_amount": "130000.00", "eliminated_auto": "100000.00", "eliminated_manual": "30000.00", + "goodwill_adjust": "0.00", "bank_amount": "568000.00", "deviation_amount": "2900.00", "comment": null +} +``` + +## 7. Открытые вопросы + +Варианты, принятые на старте, могут измениться по ответу заказчика — контракт меняется вместе с §8: OQ-180 (строки вне +бланка), OQ-181 (колонки), OQ-182 (значение без формы), OQ-183 (итоговая строка), OQ-184 (округление), OQ-185 (место +гудвилла), OQ-186 (объём расхождений), OQ-187 (ЧА), OQ-188 (нулевая база), OQ-189 (действие при превышении), +OQ-190/191 (подход по Методике, согласование клиентом — полей нет), OQ-192 (пересчёт), OQ-194 (КФО при неполном +комплекте). diff --git a/dev/api-contract-kfo.openapi.yaml b/dev/api-contract-kfo.openapi.yaml new file mode 100644 index 0000000..1f619c7 --- /dev/null +++ b/dev/api-contract-kfo.openapi.yaml @@ -0,0 +1,2547 @@ +# Сгенерировано scripts/kfo_contract/gen_kfo_openapi.py из сквозного примера §8 «КФО» (docs #206). +# Руками не править: правка генератора (и kfo_example.py) → перегенерация; текст — dev/api-contract-kfo.md. +openapi: 3.1.0 +info: + title: АРМ КА — результат КФО (AURORA-1546/1547) + version: '0.1' + description: 'Контракт до реализации (docs #206). Правила — context/05-domain-rules.md §8 «КФО»; текст — dev/api-contract-kfo.md. + Деньги — строки с двумя знаками в тыс. руб. (units).' +paths: + /api/v1/consolidations/versions/{version_id}/kfo: + get: + tags: + - consolidations + summary: Результат КФО версии + description: 'Право KFO_VIEW. Пока версия не заморожена — результат по текущим данным: устаревший пересчитывается и + сохраняется до ответа. Замороженный — только чтение сохранённого.' + operationId: kfo_get + security: + - bearerAuth: [] + parameters: &id001 + - name: version_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Результат КФО + content: + application/json: + schema: + $ref: '#/components/schemas/KfoResultOut' + examples: + goodwill_exceeded: + summary: Гудвилл включён, превышение порога ББ (сквозной пример §8) + value: + version_id: 99999999-9999-4999-8999-999999999999 + computed_at: '2026-11-10T09:15:00Z' + is_frozen: false + editable: true + read_only_reason: null + units: thousand_rub + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + name: ООО «Альфа» + inn: '7701000001' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + name: ООО «Бета» + inn: '7701000002' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + name: ООО «Гамма» + inn: '7701000003' + requisites: + company_group_name: ГК «Альфа» + related_group_name: ГСК «Альфа» + reporting_date: '2025-10-01' + version_no: 1 + version_status: CONSOLIDATION_RF + scenario: CREATE + approach: BASIC + methodology_deviations: null + uob_decision_no: null + uob_decision_date: null + corrections_balance: + count: 2 + amount: '65000.00' + corrections_income: + count: 0 + amount: '0.00' + summary: + balance: + deviation_total: '42900.00' + base_kind: NET_ASSETS + base_amount: '371000.00' + limit_percent: '10.00' + limit_amount: '37100.00' + deviation_percent: '11.56' + exceeded: true + income: + deviation_total: '0.00' + base_kind: SALES_PROFIT + base_amount: '65000.00' + limit_percent: '10.00' + limit_amount: '6500.00' + deviation_percent: '0.00' + exceeded: false + justification_required: true + threshold_justification: null + goodwill: + enabled: true + total: '18000.00' + balance: + lines: + - line_code: '1170' + line_name: Финансовые вложения + ordinal: 10 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '70000.00' + vgo_amount: '67500.00' + cleaned_amount: '2500.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '70000.00' + vgo_amount: '67500.00' + eliminated_auto: '65000.00' + eliminated_manual: '2500.00' + goodwill_adjust: '0.00' + bank_amount: '2500.00' + deviation_amount: '0.00' + comment: null + - line_code: GOODWILL + line_name: Гудвилл + ordinal: 20 + is_total: false + is_threshold_base: false + companies: [] + sum_amount: '0.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '18000.00' + bank_amount: '18000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1100' + line_name: Итого по разделу I + ordinal: 30 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '70000.00' + vgo_amount: '67500.00' + cleaned_amount: '2500.00' + share_percent: '12.20' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '70000.00' + vgo_amount: '67500.00' + eliminated_auto: '65000.00' + eliminated_manual: '2500.00' + goodwill_adjust: '18000.00' + bank_amount: '20500.00' + deviation_amount: null + comment: null + - line_code: '1230' + line_name: Дебиторская задолженность + ordinal: 40 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '520000.00' + vgo_amount: '120000.00' + cleaned_amount: '400000.00' + share_percent: '70.42' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '21.13' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.45' + sum_amount: '698000.00' + vgo_amount: '130000.00' + eliminated_auto: '100000.00' + eliminated_manual: '30000.00' + goodwill_adjust: '0.00' + bank_amount: '568000.00' + deviation_amount: '2900.00' + comment: null + - line_code: '1200' + line_name: Итого по разделу II + ordinal: 50 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '520000.00' + vgo_amount: '120000.00' + cleaned_amount: '400000.00' + share_percent: '70.42' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '21.13' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.45' + sum_amount: '698000.00' + vgo_amount: '130000.00' + eliminated_auto: '100000.00' + eliminated_manual: '30000.00' + goodwill_adjust: '0.00' + bank_amount: '568000.00' + deviation_amount: null + comment: null + - line_code: '1600' + line_name: БАЛАНС + ordinal: 60 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '590000.00' + vgo_amount: '187500.00' + cleaned_amount: '402500.00' + share_percent: '68.39' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '20.39' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.16' + sum_amount: '768000.00' + vgo_amount: '197500.00' + eliminated_auto: '165000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '18000.00' + bank_amount: '588500.00' + deviation_amount: null + comment: null + - line_code: '1310' + line_name: Уставный капитал + ordinal: 70 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '100000.00' + vgo_amount: '0.00' + cleaned_amount: '100000.00' + share_percent: '92.59' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '40000.00' + vgo_amount: '32000.00' + cleaned_amount: '8000.00' + share_percent: '7.41' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '10000.00' + vgo_amount: '10000.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '150000.00' + vgo_amount: '42000.00' + eliminated_auto: '42000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '108000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1350' + line_name: Добавочный капитал (без переоценки) + ordinal: 80 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: null + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: null + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '3000.00' + vgo_amount: '3000.00' + cleaned_amount: '0.00' + share_percent: null + sum_amount: '3000.00' + vgo_amount: '3000.00' + eliminated_auto: '3000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '0.00' + deviation_amount: '0.00' + comment: null + - line_code: '1370' + line_name: Нераспределенная прибыль (непокрытый убыток) + ordinal: 90 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '280000.00' + vgo_amount: '0.00' + cleaned_amount: '280000.00' + share_percent: '106.46' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-20000.00' + vgo_amount: '0.00' + cleaned_amount: '-20000.00' + share_percent: '-7.60' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '5000.00' + vgo_amount: '2000.00' + cleaned_amount: '3000.00' + share_percent: '1.14' + sum_amount: '265000.00' + vgo_amount: '2000.00' + eliminated_auto: '2000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '263000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1300' + line_name: Итого по разделу III + ordinal: 100 + is_total: true + is_threshold_base: true + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '380000.00' + vgo_amount: '0.00' + cleaned_amount: '380000.00' + share_percent: '102.43' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '20000.00' + vgo_amount: '32000.00' + cleaned_amount: '-12000.00' + share_percent: '-3.23' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '18000.00' + vgo_amount: '15000.00' + cleaned_amount: '3000.00' + share_percent: '0.81' + sum_amount: '418000.00' + vgo_amount: '47000.00' + eliminated_auto: '47000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '371000.00' + deviation_amount: null + comment: null + - line_code: '1410' + line_name: Заемные средства + ordinal: 110 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '0.00' + cleaned_amount: '50000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '50000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '50000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1400' + line_name: Итого по разделу IV + ordinal: 120 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '0.00' + cleaned_amount: '50000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '50000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '50000.00' + deviation_amount: null + comment: null + - line_code: '1520' + line_name: Кредиторская задолженность + ordinal: 130 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '160000.00' + vgo_amount: '10000.00' + cleaned_amount: '150000.00' + share_percent: '89.55' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '110000.00' + vgo_amount: '100000.00' + cleaned_amount: '10000.00' + share_percent: '5.97' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '30000.00' + vgo_amount: '22500.00' + cleaned_amount: '7500.00' + share_percent: '4.48' + sum_amount: '300000.00' + vgo_amount: '132500.00' + eliminated_auto: '100000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '167500.00' + deviation_amount: '40000.00' + comment: null + - line_code: '1500' + line_name: Итого по разделу V + ordinal: 140 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '160000.00' + vgo_amount: '10000.00' + cleaned_amount: '150000.00' + share_percent: '89.55' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '110000.00' + vgo_amount: '100000.00' + cleaned_amount: '10000.00' + share_percent: '5.97' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '30000.00' + vgo_amount: '22500.00' + cleaned_amount: '7500.00' + share_percent: '4.48' + sum_amount: '300000.00' + vgo_amount: '132500.00' + eliminated_auto: '100000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '167500.00' + deviation_amount: null + comment: null + - line_code: '1700' + line_name: БАЛАНС + ordinal: 150 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '590000.00' + vgo_amount: '10000.00' + cleaned_amount: '580000.00' + share_percent: '98.56' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '132000.00' + cleaned_amount: '-2000.00' + share_percent: '-0.34' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '37500.00' + cleaned_amount: '10500.00' + share_percent: '1.78' + sum_amount: '768000.00' + vgo_amount: '179500.00' + eliminated_auto: '147000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '588500.00' + deviation_amount: null + comment: null + income: + lines: + - line_code: '2110' + line_name: Выручка + ordinal: 10 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '200000.00' + vgo_amount: '10000.00' + cleaned_amount: '190000.00' + share_percent: '70.37' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '80000.00' + vgo_amount: '0.00' + cleaned_amount: '80000.00' + share_percent: '29.63' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '280000.00' + vgo_amount: '10000.00' + eliminated_auto: '10000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '270000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2120' + line_name: Себестоимость продаж + ordinal: 20 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-120000.00' + vgo_amount: '0.00' + cleaned_amount: '-120000.00' + share_percent: '70.59' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-60000.00' + vgo_amount: '-10000.00' + cleaned_amount: '-50000.00' + share_percent: '29.41' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-180000.00' + vgo_amount: '-10000.00' + eliminated_auto: '-10000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-170000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2100' + line_name: Валовая прибыль (убыток) + ordinal: 30 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '80000.00' + vgo_amount: '10000.00' + cleaned_amount: '70000.00' + share_percent: '70.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '20000.00' + vgo_amount: '-10000.00' + cleaned_amount: '30000.00' + share_percent: '30.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '100000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '100000.00' + deviation_amount: null + comment: null + - line_code: '2210' + line_name: Коммерческие расходы + ordinal: 40 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-20000.00' + vgo_amount: '0.00' + cleaned_amount: '-20000.00' + share_percent: '80.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-5000.00' + vgo_amount: '0.00' + cleaned_amount: '-5000.00' + share_percent: '20.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-25000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-25000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2220' + line_name: Управленческие расходы + ordinal: 50 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-10000.00' + vgo_amount: '0.00' + cleaned_amount: '-10000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-10000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-10000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2200' + line_name: Прибыль (убыток) от продаж + ordinal: 60 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '10000.00' + cleaned_amount: '40000.00' + share_percent: '61.54' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '15000.00' + vgo_amount: '-10000.00' + cleaned_amount: '25000.00' + share_percent: '38.46' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '65000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '65000.00' + deviation_amount: null + comment: null + warnings: + - code: FORM_MISSING + message: Нет подтверждённой формы ОФР у ООО «Гамма» + legal_entity_id: 33333333-3333-4333-8333-333333333333 + statement_type: income + count: null + goodwill_disabled: + summary: Та же версия, учёт гудвилла выключен + value: + version_id: 99999999-9999-4999-8999-999999999999 + computed_at: '2026-11-10T09:15:00Z' + is_frozen: false + editable: true + read_only_reason: null + units: thousand_rub + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + name: ООО «Альфа» + inn: '7701000001' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + name: ООО «Бета» + inn: '7701000002' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + name: ООО «Гамма» + inn: '7701000003' + requisites: + company_group_name: ГК «Альфа» + related_group_name: ГСК «Альфа» + reporting_date: '2025-10-01' + version_no: 1 + version_status: CONSOLIDATION_RF + scenario: CREATE + approach: BASIC + methodology_deviations: null + uob_decision_no: null + uob_decision_date: null + corrections_balance: + count: 2 + amount: '65000.00' + corrections_income: + count: 0 + amount: '0.00' + summary: + balance: + deviation_total: '42900.00' + base_kind: NET_ASSETS + base_amount: '363000.00' + limit_percent: '10.00' + limit_amount: '36300.00' + deviation_percent: '11.82' + exceeded: true + income: + deviation_total: '0.00' + base_kind: SALES_PROFIT + base_amount: '65000.00' + limit_percent: '10.00' + limit_amount: '6500.00' + deviation_percent: '0.00' + exceeded: false + justification_required: true + threshold_justification: null + goodwill: + enabled: false + total: '0.00' + balance: + lines: + - line_code: '1170' + line_name: Финансовые вложения + ordinal: 10 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '70000.00' + vgo_amount: '57500.00' + cleaned_amount: '12500.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '70000.00' + vgo_amount: '57500.00' + eliminated_auto: '55000.00' + eliminated_manual: '2500.00' + goodwill_adjust: '0.00' + bank_amount: '12500.00' + deviation_amount: '0.00' + comment: null + - line_code: GOODWILL + line_name: Гудвилл + ordinal: 20 + is_total: false + is_threshold_base: false + companies: [] + sum_amount: '0.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '0.00' + deviation_amount: '0.00' + comment: null + - line_code: '1100' + line_name: Итого по разделу I + ordinal: 30 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '70000.00' + vgo_amount: '57500.00' + cleaned_amount: '12500.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '70000.00' + vgo_amount: '57500.00' + eliminated_auto: '55000.00' + eliminated_manual: '2500.00' + goodwill_adjust: '0.00' + bank_amount: '12500.00' + deviation_amount: null + comment: null + - line_code: '1230' + line_name: Дебиторская задолженность + ordinal: 40 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '520000.00' + vgo_amount: '120000.00' + cleaned_amount: '400000.00' + share_percent: '70.42' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '21.13' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.45' + sum_amount: '698000.00' + vgo_amount: '130000.00' + eliminated_auto: '100000.00' + eliminated_manual: '30000.00' + goodwill_adjust: '0.00' + bank_amount: '568000.00' + deviation_amount: '2900.00' + comment: null + - line_code: '1200' + line_name: Итого по разделу II + ordinal: 50 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '520000.00' + vgo_amount: '120000.00' + cleaned_amount: '400000.00' + share_percent: '70.42' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '21.13' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.45' + sum_amount: '698000.00' + vgo_amount: '130000.00' + eliminated_auto: '100000.00' + eliminated_manual: '30000.00' + goodwill_adjust: '0.00' + bank_amount: '568000.00' + deviation_amount: null + comment: null + - line_code: '1600' + line_name: БАЛАНС + ordinal: 60 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '590000.00' + vgo_amount: '177500.00' + cleaned_amount: '412500.00' + share_percent: '71.06' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '20.67' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.27' + sum_amount: '768000.00' + vgo_amount: '187500.00' + eliminated_auto: '155000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '580500.00' + deviation_amount: null + comment: null + - line_code: '1310' + line_name: Уставный капитал + ordinal: 70 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '100000.00' + vgo_amount: '0.00' + cleaned_amount: '100000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '40000.00' + vgo_amount: '40000.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '10000.00' + vgo_amount: '10000.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '150000.00' + vgo_amount: '50000.00' + eliminated_auto: '50000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '100000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1350' + line_name: Добавочный капитал (без переоценки) + ordinal: 80 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: null + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: null + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '3000.00' + vgo_amount: '3000.00' + cleaned_amount: '0.00' + share_percent: null + sum_amount: '3000.00' + vgo_amount: '3000.00' + eliminated_auto: '3000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '0.00' + deviation_amount: '0.00' + comment: null + - line_code: '1370' + line_name: Нераспределенная прибыль (непокрытый убыток) + ordinal: 90 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '280000.00' + vgo_amount: '0.00' + cleaned_amount: '280000.00' + share_percent: '106.46' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-20000.00' + vgo_amount: '0.00' + cleaned_amount: '-20000.00' + share_percent: '-7.60' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '5000.00' + vgo_amount: '2000.00' + cleaned_amount: '3000.00' + share_percent: '1.14' + sum_amount: '265000.00' + vgo_amount: '2000.00' + eliminated_auto: '2000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '263000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1300' + line_name: Итого по разделу III + ordinal: 100 + is_total: true + is_threshold_base: true + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '380000.00' + vgo_amount: '0.00' + cleaned_amount: '380000.00' + share_percent: '104.68' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '20000.00' + vgo_amount: '40000.00' + cleaned_amount: '-20000.00' + share_percent: '-5.51' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '18000.00' + vgo_amount: '15000.00' + cleaned_amount: '3000.00' + share_percent: '0.83' + sum_amount: '418000.00' + vgo_amount: '55000.00' + eliminated_auto: '55000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '363000.00' + deviation_amount: null + comment: null + - line_code: '1410' + line_name: Заемные средства + ordinal: 110 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '0.00' + cleaned_amount: '50000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '50000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '50000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1400' + line_name: Итого по разделу IV + ordinal: 120 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '0.00' + cleaned_amount: '50000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '50000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '50000.00' + deviation_amount: null + comment: null + - line_code: '1520' + line_name: Кредиторская задолженность + ordinal: 130 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '160000.00' + vgo_amount: '10000.00' + cleaned_amount: '150000.00' + share_percent: '89.55' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '110000.00' + vgo_amount: '100000.00' + cleaned_amount: '10000.00' + share_percent: '5.97' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '30000.00' + vgo_amount: '22500.00' + cleaned_amount: '7500.00' + share_percent: '4.48' + sum_amount: '300000.00' + vgo_amount: '132500.00' + eliminated_auto: '100000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '167500.00' + deviation_amount: '40000.00' + comment: null + - line_code: '1500' + line_name: Итого по разделу V + ordinal: 140 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '160000.00' + vgo_amount: '10000.00' + cleaned_amount: '150000.00' + share_percent: '89.55' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '110000.00' + vgo_amount: '100000.00' + cleaned_amount: '10000.00' + share_percent: '5.97' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '30000.00' + vgo_amount: '22500.00' + cleaned_amount: '7500.00' + share_percent: '4.48' + sum_amount: '300000.00' + vgo_amount: '132500.00' + eliminated_auto: '100000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '167500.00' + deviation_amount: null + comment: null + - line_code: '1700' + line_name: БАЛАНС + ordinal: 150 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '590000.00' + vgo_amount: '10000.00' + cleaned_amount: '580000.00' + share_percent: '99.91' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '140000.00' + cleaned_amount: '-10000.00' + share_percent: '-1.72' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '37500.00' + cleaned_amount: '10500.00' + share_percent: '1.81' + sum_amount: '768000.00' + vgo_amount: '187500.00' + eliminated_auto: '155000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '580500.00' + deviation_amount: null + comment: null + income: + lines: + - line_code: '2110' + line_name: Выручка + ordinal: 10 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '200000.00' + vgo_amount: '10000.00' + cleaned_amount: '190000.00' + share_percent: '70.37' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '80000.00' + vgo_amount: '0.00' + cleaned_amount: '80000.00' + share_percent: '29.63' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '280000.00' + vgo_amount: '10000.00' + eliminated_auto: '10000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '270000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2120' + line_name: Себестоимость продаж + ordinal: 20 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-120000.00' + vgo_amount: '0.00' + cleaned_amount: '-120000.00' + share_percent: '70.59' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-60000.00' + vgo_amount: '-10000.00' + cleaned_amount: '-50000.00' + share_percent: '29.41' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-180000.00' + vgo_amount: '-10000.00' + eliminated_auto: '-10000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-170000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2100' + line_name: Валовая прибыль (убыток) + ordinal: 30 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '80000.00' + vgo_amount: '10000.00' + cleaned_amount: '70000.00' + share_percent: '70.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '20000.00' + vgo_amount: '-10000.00' + cleaned_amount: '30000.00' + share_percent: '30.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '100000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '100000.00' + deviation_amount: null + comment: null + - line_code: '2210' + line_name: Коммерческие расходы + ordinal: 40 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-20000.00' + vgo_amount: '0.00' + cleaned_amount: '-20000.00' + share_percent: '80.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-5000.00' + vgo_amount: '0.00' + cleaned_amount: '-5000.00' + share_percent: '20.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-25000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-25000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2220' + line_name: Управленческие расходы + ordinal: 50 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-10000.00' + vgo_amount: '0.00' + cleaned_amount: '-10000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-10000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-10000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2200' + line_name: Прибыль (убыток) от продаж + ordinal: 60 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '10000.00' + cleaned_amount: '40000.00' + share_percent: '61.54' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '15000.00' + vgo_amount: '-10000.00' + cleaned_amount: '25000.00' + share_percent: '38.46' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '65000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '65000.00' + deviation_amount: null + comment: null + warnings: + - code: FORM_MISSING + message: Нет подтверждённой формы ОФР у ООО «Гамма» + legal_entity_id: 33333333-3333-4333-8333-333333333333 + statement_type: income + count: null + frozen: + summary: 'Версия «Верифицирован»: только чтение, обоснование и комментарий заполнены' + value: + version_id: 99999999-9999-4999-8999-999999999999 + computed_at: '2026-11-10T09:15:00Z' + is_frozen: true + editable: false + read_only_reason: VERSION_NOT_EDITABLE + units: thousand_rub + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + name: ООО «Альфа» + inn: '7701000001' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + name: ООО «Бета» + inn: '7701000002' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + name: ООО «Гамма» + inn: '7701000003' + requisites: + company_group_name: ГК «Альфа» + related_group_name: ГСК «Альфа» + reporting_date: '2025-10-01' + version_no: 1 + version_status: VERIFIED + scenario: CREATE + approach: BASIC + methodology_deviations: Отклонений от Методики нет + uob_decision_no: УОБ-118 + uob_decision_date: '2025-11-20' + corrections_balance: + count: 2 + amount: '65000.00' + corrections_income: + count: 0 + amount: '0.00' + summary: + balance: + deviation_total: '42900.00' + base_kind: NET_ASSETS + base_amount: '371000.00' + limit_percent: '10.00' + limit_amount: '37100.00' + deviation_percent: '11.56' + exceeded: true + income: + deviation_total: '0.00' + base_kind: SALES_PROFIT + base_amount: '65000.00' + limit_percent: '10.00' + limit_amount: '6500.00' + deviation_percent: '0.00' + exceeded: false + justification_required: true + threshold_justification: Расхождение по 1520 — Бета → Альфа, сверка в работе; риск принят + goodwill: + enabled: true + total: '18000.00' + balance: + lines: + - line_code: '1170' + line_name: Финансовые вложения + ordinal: 10 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '70000.00' + vgo_amount: '67500.00' + cleaned_amount: '2500.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '70000.00' + vgo_amount: '67500.00' + eliminated_auto: '65000.00' + eliminated_manual: '2500.00' + goodwill_adjust: '0.00' + bank_amount: '2500.00' + deviation_amount: '0.00' + comment: null + - line_code: GOODWILL + line_name: Гудвилл + ordinal: 20 + is_total: false + is_threshold_base: false + companies: [] + sum_amount: '0.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '18000.00' + bank_amount: '18000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1100' + line_name: Итого по разделу I + ordinal: 30 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '70000.00' + vgo_amount: '67500.00' + cleaned_amount: '2500.00' + share_percent: '12.20' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '70000.00' + vgo_amount: '67500.00' + eliminated_auto: '65000.00' + eliminated_manual: '2500.00' + goodwill_adjust: '18000.00' + bank_amount: '20500.00' + deviation_amount: null + comment: null + - line_code: '1230' + line_name: Дебиторская задолженность + ordinal: 40 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '520000.00' + vgo_amount: '120000.00' + cleaned_amount: '400000.00' + share_percent: '70.42' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '21.13' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.45' + sum_amount: '698000.00' + vgo_amount: '130000.00' + eliminated_auto: '100000.00' + eliminated_manual: '30000.00' + goodwill_adjust: '0.00' + bank_amount: '568000.00' + deviation_amount: '2900.00' + comment: null + - line_code: '1200' + line_name: Итого по разделу II + ordinal: 50 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '520000.00' + vgo_amount: '120000.00' + cleaned_amount: '400000.00' + share_percent: '70.42' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '21.13' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.45' + sum_amount: '698000.00' + vgo_amount: '130000.00' + eliminated_auto: '100000.00' + eliminated_manual: '30000.00' + goodwill_adjust: '0.00' + bank_amount: '568000.00' + deviation_amount: null + comment: null + - line_code: '1600' + line_name: БАЛАНС + ordinal: 60 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '590000.00' + vgo_amount: '187500.00' + cleaned_amount: '402500.00' + share_percent: '68.39' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '10000.00' + cleaned_amount: '120000.00' + share_percent: '20.39' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '0.00' + cleaned_amount: '48000.00' + share_percent: '8.16' + sum_amount: '768000.00' + vgo_amount: '197500.00' + eliminated_auto: '165000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '18000.00' + bank_amount: '588500.00' + deviation_amount: null + comment: null + - line_code: '1310' + line_name: Уставный капитал + ordinal: 70 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '100000.00' + vgo_amount: '0.00' + cleaned_amount: '100000.00' + share_percent: '92.59' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '40000.00' + vgo_amount: '32000.00' + cleaned_amount: '8000.00' + share_percent: '7.41' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '10000.00' + vgo_amount: '10000.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '150000.00' + vgo_amount: '42000.00' + eliminated_auto: '42000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '108000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1350' + line_name: Добавочный капитал (без переоценки) + ordinal: 80 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: null + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: null + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '3000.00' + vgo_amount: '3000.00' + cleaned_amount: '0.00' + share_percent: null + sum_amount: '3000.00' + vgo_amount: '3000.00' + eliminated_auto: '3000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '0.00' + deviation_amount: '0.00' + comment: null + - line_code: '1370' + line_name: Нераспределенная прибыль (непокрытый убыток) + ordinal: 90 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '280000.00' + vgo_amount: '0.00' + cleaned_amount: '280000.00' + share_percent: '106.46' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-20000.00' + vgo_amount: '0.00' + cleaned_amount: '-20000.00' + share_percent: '-7.60' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '5000.00' + vgo_amount: '2000.00' + cleaned_amount: '3000.00' + share_percent: '1.14' + sum_amount: '265000.00' + vgo_amount: '2000.00' + eliminated_auto: '2000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '263000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1300' + line_name: Итого по разделу III + ordinal: 100 + is_total: true + is_threshold_base: true + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '380000.00' + vgo_amount: '0.00' + cleaned_amount: '380000.00' + share_percent: '102.43' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '20000.00' + vgo_amount: '32000.00' + cleaned_amount: '-12000.00' + share_percent: '-3.23' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '18000.00' + vgo_amount: '15000.00' + cleaned_amount: '3000.00' + share_percent: '0.81' + sum_amount: '418000.00' + vgo_amount: '47000.00' + eliminated_auto: '47000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '371000.00' + deviation_amount: null + comment: null + - line_code: '1410' + line_name: Заемные средства + ordinal: 110 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '0.00' + cleaned_amount: '50000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '50000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '50000.00' + deviation_amount: '0.00' + comment: null + - line_code: '1400' + line_name: Итого по разделу IV + ordinal: 120 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '0.00' + cleaned_amount: '50000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '50000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '50000.00' + deviation_amount: null + comment: null + - line_code: '1520' + line_name: Кредиторская задолженность + ordinal: 130 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '160000.00' + vgo_amount: '10000.00' + cleaned_amount: '150000.00' + share_percent: '89.55' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '110000.00' + vgo_amount: '100000.00' + cleaned_amount: '10000.00' + share_percent: '5.97' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '30000.00' + vgo_amount: '22500.00' + cleaned_amount: '7500.00' + share_percent: '4.48' + sum_amount: '300000.00' + vgo_amount: '132500.00' + eliminated_auto: '100000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '167500.00' + deviation_amount: '40000.00' + comment: 'Расхождение Бета → Альфа по 62/60: акт сверки на 01.10 не получен' + - line_code: '1500' + line_name: Итого по разделу V + ordinal: 140 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '160000.00' + vgo_amount: '10000.00' + cleaned_amount: '150000.00' + share_percent: '89.55' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '110000.00' + vgo_amount: '100000.00' + cleaned_amount: '10000.00' + share_percent: '5.97' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '30000.00' + vgo_amount: '22500.00' + cleaned_amount: '7500.00' + share_percent: '4.48' + sum_amount: '300000.00' + vgo_amount: '132500.00' + eliminated_auto: '100000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '167500.00' + deviation_amount: null + comment: null + - line_code: '1700' + line_name: БАЛАНС + ordinal: 150 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '590000.00' + vgo_amount: '10000.00' + cleaned_amount: '580000.00' + share_percent: '98.56' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '130000.00' + vgo_amount: '132000.00' + cleaned_amount: '-2000.00' + share_percent: '-0.34' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '48000.00' + vgo_amount: '37500.00' + cleaned_amount: '10500.00' + share_percent: '1.78' + sum_amount: '768000.00' + vgo_amount: '179500.00' + eliminated_auto: '147000.00' + eliminated_manual: '32500.00' + goodwill_adjust: '0.00' + bank_amount: '588500.00' + deviation_amount: null + comment: null + income: + lines: + - line_code: '2110' + line_name: Выручка + ordinal: 10 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '200000.00' + vgo_amount: '10000.00' + cleaned_amount: '190000.00' + share_percent: '70.37' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '80000.00' + vgo_amount: '0.00' + cleaned_amount: '80000.00' + share_percent: '29.63' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '280000.00' + vgo_amount: '10000.00' + eliminated_auto: '10000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '270000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2120' + line_name: Себестоимость продаж + ordinal: 20 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-120000.00' + vgo_amount: '0.00' + cleaned_amount: '-120000.00' + share_percent: '70.59' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-60000.00' + vgo_amount: '-10000.00' + cleaned_amount: '-50000.00' + share_percent: '29.41' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-180000.00' + vgo_amount: '-10000.00' + eliminated_auto: '-10000.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-170000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2100' + line_name: Валовая прибыль (убыток) + ordinal: 30 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '80000.00' + vgo_amount: '10000.00' + cleaned_amount: '70000.00' + share_percent: '70.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '20000.00' + vgo_amount: '-10000.00' + cleaned_amount: '30000.00' + share_percent: '30.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '100000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '100000.00' + deviation_amount: null + comment: null + - line_code: '2210' + line_name: Коммерческие расходы + ordinal: 40 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-20000.00' + vgo_amount: '0.00' + cleaned_amount: '-20000.00' + share_percent: '80.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '-5000.00' + vgo_amount: '0.00' + cleaned_amount: '-5000.00' + share_percent: '20.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-25000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-25000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2220' + line_name: Управленческие расходы + ordinal: 50 + is_total: false + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '-10000.00' + vgo_amount: '0.00' + cleaned_amount: '-10000.00' + share_percent: '100.00' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '-10000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '-10000.00' + deviation_amount: '0.00' + comment: null + - line_code: '2200' + line_name: Прибыль (убыток) от продаж + ordinal: 60 + is_total: true + is_threshold_base: false + companies: + - legal_entity_id: 11111111-1111-4111-8111-111111111111 + client_amount: '50000.00' + vgo_amount: '10000.00' + cleaned_amount: '40000.00' + share_percent: '61.54' + - legal_entity_id: 22222222-2222-4222-8222-222222222222 + client_amount: '15000.00' + vgo_amount: '-10000.00' + cleaned_amount: '25000.00' + share_percent: '38.46' + - legal_entity_id: 33333333-3333-4333-8333-333333333333 + client_amount: '0.00' + vgo_amount: '0.00' + cleaned_amount: '0.00' + share_percent: '0.00' + sum_amount: '65000.00' + vgo_amount: '0.00' + eliminated_auto: '0.00' + eliminated_manual: '0.00' + goodwill_adjust: '0.00' + bank_amount: '65000.00' + deviation_amount: null + comment: null + warnings: + - code: FORM_MISSING + message: Нет подтверждённой формы ОФР у ООО «Гамма» + legal_entity_id: 33333333-3333-4333-8333-333333333333 + statement_type: income + count: null + '403': + description: Нет права KFO_VIEW + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorOut' + '404': + description: Версия не найдена или вне видимости — NOT_FOUND + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorOut' + /api/v1/consolidations/versions/{version_id}/kfo/annotations: + put: + tags: + - consolidations + summary: Ручные поля КФО + description: Право CONSOLIDATION_EDIT, блокировка версии. Комментарии строк, обоснование порога, причины отклонений + от Методики — одной транзакцией; отсутствующее поле не меняется, null очищает. Аудит KFO_EDIT. Ответ — KfoResultOut + целиком. + operationId: kfo_annotations_put + security: + - bearerAuth: [] + parameters: *id001 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/KfoAnnotationsIn' + examples: + justify: + summary: Обоснование и комментарий строки + value: + comments: + - statement_type: balance + line_code: '1520' + comment: 'Расхождение Бета → Альфа по 62/60: акт сверки на 01.10 не получен' + threshold_justification: Расхождение по 1520 — Бета → Альфа, сверка в работе; риск принят + responses: + '200': + description: Результат КФО после сохранения + content: + application/json: + schema: + $ref: '#/components/schemas/KfoResultOut' + '403': + description: Нет права CONSOLIDATION_EDIT + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorOut' + '404': + description: NOT_FOUND + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorOut' + '409': + description: VERSION_NOT_EDITABLE | LOCK_REQUIRED | LOCKED_BY_OTHER + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorOut' + '422': + description: UNKNOWN_LINE (detail.statement_type, detail.line_code) или массив ошибок валидации тела (длины 2000/4000) + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ErrorOut' + - type: object +components: + schemas: + KfoLineCompanyOut: + type: object + additionalProperties: false + properties: + legal_entity_id: + type: string + format: uuid + client_amount: + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: &id002 + - '588500.00' + description: «По данным клиента», тыс. руб.; строки расходов приведены к −|x| (§8 «КФО» п. 2) + vgo_amount: + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: *id002 + description: «В т. ч. взаимные», тыс. руб. до целых, со знаком строки (п. 6) + cleaned_amount: + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: *id002 + description: «Очищенные показатели» = client_amount − vgo_amount + share_percent: + type: + - string + - 'null' + pattern: ^-?\d{1,3}\.\d{2}$ + description: «Доля в показателе по группе, %» от bank_amount строки, ROUND_HALF_UP; null при нулевом итоге или |доля| + ≥ 1000 % + required: + - legal_entity_id + - client_amount + - vgo_amount + - cleaned_amount + - share_percent + KfoLineOut: + type: object + additionalProperties: false + properties: + line_code: + type: string + description: код строки бланка редакции на отчётную дату или GOODWILL + line_name: + type: string + ordinal: + type: integer + description: порядок строки в бланке + is_total: + type: boolean + description: итоговая строка бланка (1100, 1600, 2100 …) + is_threshold_base: + type: boolean + description: строка — база порога (1300, 1600, 2110 или 2200), подсветка при превышении + companies: + type: array + items: + $ref: '#/components/schemas/KfoLineCompanyOut' + description: по компаниям в порядке KfoResultOut.companies; у GOODWILL — пусто + sum_amount: + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: *id002 + description: Σ client_amount + vgo_amount: + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: *id002 + description: Σ vgo_amount = eliminated_auto + eliminated_manual + eliminated_auto: &id003 + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: *id002 + eliminated_manual: *id003 + goodwill_adjust: + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: *id002 + description: Σ G — у GOODWILL и итогов 1100, 1600 + bank_amount: + type: string + pattern: ^-?\d{1,16}\.\d{2}$ + examples: *id002 + description: «КФО, подготовленная банком» = sum_amount − vgo_amount + goodwill_adjust + deviation_amount: + type: + - string + - 'null' + pattern: ^-?\d{1,16}\.\d{2}$ + description: «Расхождения» (§8 «КФО» п. 9); у итогов — null + comment: + type: + - string + - 'null' + maxLength: 2000 + required: + - line_code + - line_name + - ordinal + - is_total + - is_threshold_base + - companies + - sum_amount + - vgo_amount + - eliminated_auto + - eliminated_manual + - goodwill_adjust + - bank_amount + - deviation_amount + - comment + KfoStatementOut: + type: object + additionalProperties: false + properties: + lines: + type: array + items: + $ref: '#/components/schemas/KfoLineOut' + required: + - lines + KfoThresholdOut: + type: object + additionalProperties: false + properties: + deviation_total: *id003 + base_kind: + type: string + enum: + - NET_ASSETS + - BALANCE_TOTAL + - SALES_PROFIT + - REVENUE + base_amount: *id003 + limit_percent: + type: string + enum: + - '10.00' + - '1.00' + limit_amount: *id003 + deviation_percent: + type: + - string + - 'null' + pattern: ^-?\d+\.\d{2}$ + description: deviation_total / base_amount × 100; null при нулевой базе + exceeded: + type: boolean + description: deviation_total > limit_amount (равно — норма) + required: + - deviation_total + - base_kind + - base_amount + - limit_percent + - limit_amount + - deviation_percent + - exceeded + KfoSummaryOut: + type: object + additionalProperties: false + properties: + balance: + $ref: '#/components/schemas/KfoThresholdOut' + income: + $ref: '#/components/schemas/KfoThresholdOut' + required: + - balance + - income + KfoCompanyOut: + type: object + additionalProperties: false + properties: + legal_entity_id: + type: string + format: uuid + name: + type: string + inn: + type: string + required: + - legal_entity_id + - name + - inn + KfoCorrectionsOut: + type: object + additionalProperties: false + properties: + count: + type: integer + minimum: 0 + amount: *id003 + required: + - count + - amount + description: агрегаты с применённой корректировкой и Σ их исключений, тыс. руб. + KfoRequisitesOut: + type: object + additionalProperties: false + properties: + company_group_name: + type: string + related_group_name: + type: + - string + - 'null' + reporting_date: + type: string + format: date + version_no: + type: integer + version_status: + type: string + description: consolidation_version.status (enum consolidation_status DBML) + enum: + - INITIATED + - DOCUMENTS_PROCESSING + - DATA_CORRECTION + - CONSOLIDATION_RF + - ADJUSTMENT + - VERIFICATION_RF + - CONSOLIDATION_GO + - VERIFICATION_GO + - VERIFIED + - CANCELLED + - REJECTED + - ARCHIVED + scenario: + type: string + enum: + - CREATE + - CONFIRM + approach: + type: string + enum: + - BASIC + - EXTENDED + methodology_deviations: + type: + - string + - 'null' + maxLength: 4000 + uob_decision_no: + type: + - string + - 'null' + uob_decision_date: + type: + - string + - 'null' + format: date + corrections_balance: + $ref: '#/components/schemas/KfoCorrectionsOut' + corrections_income: + $ref: '#/components/schemas/KfoCorrectionsOut' + required: + - company_group_name + - related_group_name + - reporting_date + - version_no + - version_status + - scenario + - approach + - methodology_deviations + - uob_decision_no + - uob_decision_date + - corrections_balance + - corrections_income + KfoGoodwillOut: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + total: *id003 + required: + - enabled + - total + KfoWarningOut: + type: object + additionalProperties: false + properties: + code: + type: string + enum: + - FORM_MISSING + - FORM_AMBIGUOUS + - FORM_UNITS_UNKNOWN + - LINE_OUTSIDE_BLANK + - UNALLOCATED_ROWS + - INCOME_RULES_MISSING + - GOODWILL_NOT_APPLIED + - GOODWILL_ROW_REASSIGNED + - CORRECTION_SUPERSEDED_BY_GOODWILL + message: + type: string + legal_entity_id: + type: + - string + - 'null' + format: uuid + statement_type: + type: + - string + - 'null' + enum: + - balance + - income + - null + count: + type: + - integer + - 'null' + required: + - code + - message + KfoResultOut: + type: object + additionalProperties: false + properties: + version_id: + type: string + format: uuid + computed_at: + type: string + format: date-time + is_frozen: + type: boolean + editable: + type: boolean + read_only_reason: + type: + - string + - 'null' + enum: + - VERSION_NOT_EDITABLE + - LOCK_REQUIRED + - LOCKED_BY_OTHER + - null + units: + type: string + const: thousand_rub + companies: + type: array + items: + $ref: '#/components/schemas/KfoCompanyOut' + requisites: + $ref: '#/components/schemas/KfoRequisitesOut' + summary: + $ref: '#/components/schemas/KfoSummaryOut' + justification_required: + type: boolean + threshold_justification: + type: + - string + - 'null' + maxLength: 4000 + goodwill: + $ref: '#/components/schemas/KfoGoodwillOut' + balance: + $ref: '#/components/schemas/KfoStatementOut' + income: + $ref: '#/components/schemas/KfoStatementOut' + warnings: + type: array + items: + $ref: '#/components/schemas/KfoWarningOut' + required: + - version_id + - computed_at + - is_frozen + - editable + - read_only_reason + - units + - companies + - requisites + - summary + - justification_required + - threshold_justification + - goodwill + - balance + - income + - warnings + KfoCommentIn: + type: object + additionalProperties: false + properties: + statement_type: + type: string + enum: + - balance + - income + line_code: + type: string + comment: + type: + - string + - 'null' + maxLength: 2000 + required: + - statement_type + - line_code + - comment + KfoAnnotationsIn: + type: object + additionalProperties: false + properties: + comments: + type: array + items: + $ref: '#/components/schemas/KfoCommentIn' + threshold_justification: + type: + - string + - 'null' + maxLength: 4000 + methodology_deviations: + type: + - string + - 'null' + maxLength: 4000 + required: [] + description: Отсутствующее поле не меняется, null — очищает + ErrorDetail: + type: object + additionalProperties: false + properties: + code: + type: string + message: + type: string + line_code: + type: + - string + - 'null' + statement_type: + type: + - string + - 'null' + required: + - code + - message + ErrorOut: + type: object + additionalProperties: false + properties: + detail: + $ref: '#/components/schemas/ErrorDetail' + required: + - detail + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/dev/frontend-api-guide.md b/dev/frontend-api-guide.md index 0e7b9ea..3cb9016 100644 --- a/dev/frontend-api-guide.md +++ b/dev/frontend-api-guide.md @@ -639,7 +639,8 @@ variance_percent?, exceeds_threshold, one_sided, skipped_osv_entry_count, osv_en amount_b, deviation_amount, deviation_percent?, eliminated_amount, exceeds_threshold, status, osv_entry_ids_a[], osv_entry_ids_b[]}}`, порядок — `rule_no`, затем порядок компаний периметра. `status`: `AUTO` — элиминируется `eliminated_amount = min(ΣA, ΣB)` (расхождение ≤ 5 %); `DISCREPANCY` — обе стороны есть, расхождение > 5 % или меньшая сторона -≤ 0, элиминация 0; `ONE_SIDED` — одна из сумм нулевая; `MANUAL` — правило `elimination_mode = manual` (№ 1, № 2), элиминация 0. +≤ 0, элиминация 0; `ONE_SIDED` — одна из сумм нулевая; `MANUAL` — правило `elimination_mode = manual` (№ 1), элиминация 0. Правило № 2 после правки seed в AURORA-1546 — +`min_or_full` (DBML v5.26): `AUTO` при обеих сторонах, `ONE_SIDED` при пустой стороне 2 с `eliminated_amount = ΣA` (§8 «КФО» п. 4). `osv_entry_ids_*` — строки ОСВ, вошедшие в ΣA и ΣB (по ним — переход в сводную ОСВ). `EliminationPairsOut` — `{version_no, rule_id, rule_no, company_a_id, company_b_id, amount_a, amount_b, deviation_amount, @@ -653,6 +654,13 @@ legal_entity_id, counterparty_id, account_id, account_code, closing_debit?, clos Суммы — строки в рублях с двумя знаками, со знаком (как в шахматке); `deviation_amount = |ΣA − ΣB|`, `deviation_percent` — к меньшей стороне, `null`, если она ≤ 0. Упаковка сумму элиминации не меняет: в КФО идёт `eliminated_amount` тройки. +### 5.7.3 Результат КФО (AURORA-1546/1547; контракт до реализации) + +Вкладка «КФО» отчёта версии: `GET …/kfo` (право `KFO_VIEW`) и `PUT …/kfo/annotations` (`CONSOLIDATION_EDIT`, блокировка +версии). **Деньги — в тыс. руб.** (`units = "thousand_rub"`), не в рублях, как в остальных разделах. Полностью — +`dev/api-contract-kfo.md` и `dev/api-contract-kfo.openapi.yaml` (три полных примера ответа для фейка); правила — +`05-domain-rules.md` §8 «КФО». + ### 5.8 Схемы ответов (кратко) Поля с `?` могут быть `null`, но ключ присутствует всегда. Полные схемы — `/openapi.json` и контракты по областям. diff --git a/model/data-model-v5.26.dbml b/model/data-model-v5.26.dbml new file mode 100644 index 0000000..96cbd50 --- /dev/null +++ b/model/data-model-v5.26.dbml @@ -0,0 +1,1614 @@ +// ДКБ · Модель данных v5.26: справочники, файлы/распознавание, консолидация, КФО, аудит, ЕХФД +// Формат: DBML (dbdiagram.io). Вставить на https://dbdiagram.io/d +// >>> Источник истины по модели данных. Аналитика в Confluence ссылается сюда. +// Над каждой таблицей — блок «ИСТОЧНИК»: пункт ТЗ АРМ КА BIQ-34 / ТЗ ЕХФД (помечено «ЕХ ТЗ») / Методика; +// страница аналитики (md); экран макета dkb_24.09.26; версия модели, в которой таблица появилась (v4 = исходная DBML аналитики). +// +// v5.26 (расчёт КФО, AURORA-1546/1547; RSHB/arm-ka-docs#206; одна миграция arm-core — в AURORA-1546): +// Номер v5.25 занят PR #203 (факты качества первички, AURORA-1759), ещё не слит; v5.26 построена от v5.24 — +// второй слитый PR вливает изменения первого в свой файл, номера задним числом не меняются. +// 1. kfo_result_line_company: + client_amount («по данным клиента», тыс. руб.), + vgo_amount («в т. ч. взаимные», со +// знаком строки); amount — «очищенные показатели» (context/05-domain-rules.md §8 «КФО», п. 6–7). Без них колонки +// экрана КФО не восстановить из сохранённого результата. +// 2. kfo_result: + warnings jsonb (предупреждения расчёта, §8 «КФО» п. 12), + corrections_count_bb/_ofr, +// corrections_amount_bb/_ofr (сводка корректировок для реквизитов формы, п. 11) — замороженная версия отдаёт то, +// что видели при верификации. +// 3. audit_action: + KFO_EDIT — сохранение ручных полей КФО (комментарии строк, обоснование порога, причины +// отклонений от Методики), раздел KFO. +// 4. elimination_rule.elimination_mode: значение min_or_full (правило ББ 2 по DOC-274230: min при обеих сторонах, +// сторона 1 при пустой стороне 2, без порога; §5 п. 1, §8 «КФО» п. 4). varchar — схема не меняется; seed v1 +// правится на месте в AURORA-1546 (верифицированных версий нет). +// 5. kfo_result_line: пояснения приведены к §8 «КФО» (единицы — тыс. руб.; eliminated_auto / eliminated_manual — от +// агрегатов без / с корректировкой, по одному агрегату не складываются; goodwill_adjust — у GOODWILL и итогов; +// consolidated_amount = sum − vgo + goodwill_adjust). Структура не меняется. +// Миграция: новые колонки nullable, значение enum; откат удаляет колонки, KFO_EDIT при наличии событий не +// откатывается (как FILE_REPROCESS в v5.21). +// +// v5.24 (AURORA-1751/1540/1541, RSHB/arm-ka-docs#196; одна миграция arm-core, уже слита main): +// 1. Новая таблица vgo_assignment: ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения +// по правилам ВГО (context/05-domain-rules.md §5) — привязка к правилу (ASSIGN), снятие привязки (UNASSIGNED), +// отметка «не учитывать в КФО» (is_exclude) у нераспределённой строки. Автоматическое разнесение по-прежнему не +// хранится (DECISION-5) — только это, поверх него. Одна действующая запись на (версия, тип отчётности, +// osv_entry_id); повторное решение обновляет её, а не добавляет новую. +// 2. Черновик контракта (dev/api-contract-corrections.md §5) предполагал отдельное поле `side` — строка с +// одновременно дебетовым и кредитовым сальдо даёт две позиции. В уже слитой миграции 0011 (arm-core) поля нет: +// решение по такой строке одно на обе позиции, не по стороне отдельно. +// Реализация полностью в коде: сервис `VgoAssignmentCrudService`, эндпоинты `PUT|DELETE …/vgo-assignments/rule`, +// `PUT …/vgo-assignments/exclude`; участвует в таблице корректировок (`CorrectionAggregateSource`, §8). +// +// v5.23 (решения владельца 23.10.2026; RSHB/arm-ka-docs#164, AURORA-1536, ревью arm-core #175; одна миграция arm-core): +// 1. consolidation_version.elimination_rules_version: not null. Версия правил элиминации (elimination_rule.version_no), +// действующая на дату создания версии консолидации (OQ-174), фиксируется при создании и дальше не меняется; по ней +// считаются шахматка-пресет и пары ВГО (context/05-domain-rules.md §4, §5). Пустое значение больше не допускается: +// «пока поле пусто — последние открытые правила» убрано, версия без правил не создаётся. +// Миграция: существующим версиям — номер версии правил, действовавшей на дату created_at (по Москве), при её отсутствии — +// последней открытой; затем NOT NULL. Откат снимает только NOT NULL, значения остаются. +// +// v5.22 (22.10.2026; AURORA-1446): +// 1. comment.object_type: убраны устаревшие chess_cell и kfo_result_line. +// Сохранено описание привязки: document | consolidation_version | …. +// Комментарий к правке пары ВГО — correction.comment; к строкам КФО — kfo_result_line.comment +// и client_kfo_line.comment (отдельные поля, не лента комментариев). +// Уточнены описание и источники comment. Структура БД не меняется, миграция не требуется. +// +// v5.21 (решение владельца 22.10.2026; RSHB/arm-ka-docs#149, AURORA-1442; одна миграция arm-core): +// 1. audit_action: значение FILE_REPROCESS — повторная обработка файла пользователем (раздел PRIMARY_DOCUMENTS; +// context/05-domain-rules.md §9.5 п. 6, п. 13). Файл не загружается заново — создаётся новое recognition_job +// (attempt + 1) и становится current_recognition_job_id, поэтому FILE_UPLOAD (приём нового файла) и FILE_REPLACE +// (автозамена другим файлом) не подходят. Событие — на файл: object_type = stored_file, before_value — прежнее +// задание, after_value — новое; пакет версии пишет по событию на каждый отправленный файл. +// Миграция только добавляет значение enum. Откат при наличии событий FILE_REPROCESS останавливается с ошибкой и ничего +// не меняет (журнал append-only). +// +// v5.20 (решения владельца 21.10.2026; RSHB/arm-ka-docs#147, OQ-161 вариант 2, AURORA-1442; одна миграция arm-core): +// 1. document: is_modified (boolean, not null, default false), original_values (jsonb), modified_by (FK → app_user), +// modified_at — уточнение периода и счёта документа ОСВ аналитиком (context/05-domain-rules.md §9.5 п. 12). +// Правка — на месте в period_year / period_quarter / is_cumulative / osv_account_code; при первой правке прежние +// значения этих четырёх полей копируются в original_values и дальше не меняются (первичка не мутируется, как +// osv_entry.original_values). Копия document не создаётся: file_ordinal — номер документа в результате +// распознавания, не номер редакции. Строки osv_entry при смене счёта не меняются. +// Миграция только добавляет колонки (is_modified = false, остальные NULL). Откат на данных с is_modified = true +// останавливается с ошибкой и ничего не меняет (потерялись бы исходные значения). +// +// v5.19 (решения владельца 17.10.2026, Р-1…Р-19; RSHB/arm-ka-docs#116, openspec/changes/archive/2026-10-19-unified-statement-normalization; +// контракт результата v2.0; одна миграция arm-core): +// 1. financial_statement_line: снята уникальность (meta_id, line_code) — повтор кода законен, хранятся все строки (Р-4); +// line_code остаётся nullable и пуст у строк unreadable / missing / detail (Р-8, отменяет OQ-36 / OQ-141). +// Новые колонки: row_index (порядок строки в документе; у шаблонных и созданных правкой — NULL), source_ref +// (положение в источнике, частичный уникальный индекс (meta_id, source_ref) WHERE source_ref IS NOT NULL), +// line_status (enum statement_line_status), line_code_text (сырой текст ячейки кода), parent_row_index (родитель +// detail), needs_review (как пришло от сервиса, пересчитывается ядром после правок), is_confirmed (код подтверждён +// аналитиком). +// 2. financial_statement_value: source_text (values[].text — сырой текст источника), needs_review (как пришло, +// пересчитывается после правок; пересматривает п. 4 v5.13 «считается, не хранится»), is_confirmed + confirmed_by / +// confirmed_at (подтверждено аналитиком без изменения значения). +// 3. financial_statement_column: period_kind (absolute | relative:<вид> | none), label_confidence. +// 4. financial_statement_meta: form_variant (enum statement_form_variant), needs_review_fields (поля шапки с +// needs_review, jsonb-массив имён). +// 5. Новая таблица document_issue (+ enum'ы document_issue_kind, issue_severity, issue_source): предупреждения и нарушения +// §7 документа как пришли от сервиса или посчитаны ядром; очищается при перезагрузке файла; при пересчёте после +// правки записи relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой. +// 6. recognition_job.statements_version — коммит копии общего кода arm_statements, которым посчитан результат. +// 7. document: уникальность (stored_file_id, document_type, osv_account_code) — только для типов кроме BALANCE / INCOME +// (частичный индекс; ОСВ по счёту, DISCLOSURE, OTHER — «один на тип на файл», §9.5 п. 2); вторая форма того же типа +// в файле хранится (Р-10, Р-15); новая колонка file_ordinal — индекс документа в documents[] результата (у всех +// документов при любом MAJOR контракта; несозданный документ номер не занимает), уникален в файле. +// 8. related_group.crm_id (varchar, nullable, unique) — ID ГСК/ГСЗ в ЕЦП CRM (AURORA-1619, arm-core#112, OQ-33). +// 9. Признак «строка создана правкой аналитика» — financial_statement_line.row_index IS NULL (заменяет прежний +// признак line_code_original_value IS NULL: у строк unreadable / missing / detail источника кода тоже нет). +// line_code_original_value — код строки из результата как пришёл (NULL у статусов кроме ok и у строк, созданных +// правкой); правками не меняется. +// 10. financial_statement_meta: header_source (jsonb — сырой текст и уверенность всех полей шапки результата, +// {"<поле>": {"text", "confidence"}}), organization_name_original_value и organization_name_modified (наименование +// правится PATCH шапки, как ИНН). +// 11. document_issue: codes (jsonb — коды цели и членов нарушения §7), column_index (столбец предупреждения вне колонок +// формы — COLUMN_SKIPPED и др.). +// Перед выкладкой база очищается от форм ББ/ОФР (решение владельца 18.10.2026, Р-24; скрипт ядра +// scripts/purge_statements.py): миграция только добавляет колонки, таблицу, enum'ы, индексы и значения по умолчанию, +// §7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): row_index строк — NULL у +// строк с line_code_original_value IS NULL (созданных правкой), у остальных — по возрастанию line_code внутри формы с 0; +// line_status = ok, needs_review строк = false, needs_review значений = НЕ values_ok (values_ok — правлено или +// is_recognized не false и confidence 100 / NULL), source_text = string_value, is_confirmed = false, period_kind = +// absolute при period_date, иначе none, form_variant = NULL (набор §7 — полный), header_source — из *_original_value +// и inn_confidence, organization_name_original_value = organization_name, file_ordinal — по (created_at, id) внутри +// файла с 0 (у всех документов), related_group.crm_id = NULL; document_issue — пуст: корректность нарушений у форм, +// оставшихся в неочищенной базе, не гарантируется до первой правки. +// Откат гарантирован на пустой БД и на данных, которые v5.18 представляет без потери первичных данных и ручного ввода; +// downgrade останавливается с ошибкой и ничего не меняет при повторе кода в форме, строке без кода, двух формах одного +// типа в файле, непустом related_group.crm_id, organization_name_modified = true; производные и сигнальные колонки +// v5.19 и document_issue удаляет с записью числа удалённых значений в журнал миграции. +// +// v5.18 (решения владельца 09.10.2026; RSHB/arm-ka-docs#90, openspec/changes/console-files): +// 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте +// которой загружен файл, — основа «рабочего стола консоли» (полный состав стола — context/05-domain-rules.md §9.5 п. 11), общего для всех версий периода. NULL — загрузка без контекста +// (страница ЮЛ, монозаёмщики, API). Пишется один раз при загрузке (POST /consolidations/{id}/legal-entities/{le}/files); +// при автозамене новый файл без контекста наследует consolidation_id заменённого. Индекс (legal_entity_id, consolidation_id). +// Что использует конкретная версия, по-прежнему определяет consolidation_document; заморозка после VERIFIED — привязки и ex_snapshot. +// Миграция 0006 arm-core: backfill по consolidation_document (при нескольких периодах — самый ранний). +// 2. Note recognition_job.reply_deadline: срок ответа OCR — 20 мин (OQ-159, решение владельца 09.10.2026), структура не меняется. +// +// v5.17 (решение владельца 07.10.2026, пересмотр OQ-31; RSHB/arm-ka-docs#53): +// 1. legal_entity.kpp и legal_entity_history.kpp (varchar, nullable): КПП юридического лица. Формат — 9 символов: +// 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры; иначе INVALID_KPP. У ИП КПП запрещён (INVALID_KPP; +// импорт — строка пропускается). Для ЮЛ необязателен: существующие записи и импорт без колонки КПП остаются валидными. +// Уникальность — только legal_entity.inn (КПП — атрибут, сопоставление контрагентов ОСВ по ИНН не меняется). +// Изменение КПП — обычная правка со снимком в legal_entity_history. Отменяет п. 2 v5.14 («КПП не вводится»). +// +// v5.16 (Срез 4, решения владельца 06.10.2026): +// 1. Новый enum counterparty_match_kind и колонка osv_entry.counterparty_match_kind (nullable): способ, которым строке ОСВ +// сопоставлен контрагент. NULL ⇔ counterparty_id NULL; INN / NAME / ALIAS — шаг автоматического резолва при маппинге +// (ИНН; точное имя; нормализованное имя или алиас); MANUAL — ручное сопоставление аналитиком; PROPAGATED — +// автоприменение по имени в версии (DOC-221278 п. 12–13). Миграция 0004 заполняет колонку по уже смапленным строкам. +// 2. Без изменений структуры, уточнение семантики consolidation_document: строка создаётся при подтверждении документа +// (по документам ЮЛ — членов периметра версии «в работе»), при подходящем подтверждённом документе — при добавлении ЮЛ +// в периметр; удаляется при отвязке документа (замена файла, удаление, исключение ЮЛ из периметра). Для одной +// (ЮЛ, document_type, osv_account_code, период, признак нарастающего) привязан один документ — самый поздний confirmed_at. +// 3. Без изменений структуры, уточнение семантики consolidation_lock: блокировка без истечения по времени — активна, пока её не +// снял владелец, администратор или перехват другим пользователем с CONSOLIDATION_EDIT по причине (OQ-119). +// +// v5.15 (Срез 2c, решения владельца 05.10.2026): +// 1. osv_data_type: добавлено NU — строки налогового учёта из ОСВ «БУ, НУ» хранятся в osv_entry как есть (OQ-45); +// контракт результата допускал NU с v1.0. +// 2. app_user.email хранится в нижнем регистре: нормализация при записи (seed, администрирование), поиск по +// lower(email) остаётся; уникальность — существующий UNIQUE по email (OQ-59). Структура не меняется. +// 3. Без изменений структуры, уточнение семантики: stored_file.status — PROCESSING ставится inbox'ом по ack сервиса, +// PROCESSED/ERROR — циклом маппинга результата (OQ-67); маппинг выполняется отдельно от inbox (OQ-69); +// recognition_job.group_schema заполняет маппер; «Итого» в osv_entry не хранится (OQ-01). +// 4. Индексы для удаления файла и обхода дерева строк: osv_entry (parent_entry_id), osv_entry (recognition_job_id), +// stored_file (current_recognition_job_id) — проверки FK при DELETE документа/файла и очистке перед повторным +// маппингом шли seq scan'ом (OQ-117). Замер: DELETE документа 1000 строк 8,6 с → 16 мс на 200 тыс. строк. +// +// v5.14 (Срез 1, решения владельца 04.10.2026): +// 1. ЮЛ может состоять ОДНОВРЕМЕННО в одной ГСК и одной ГСЗ (но только в одной группе каждого вида) — отменяет +// ANL-030/v5.8. В related_group_member возвращена денормализованная колонка kind (= related_group.kind, +// равенство проверяет ядро при записи); частичный уникальный индекс стал (legal_entity_id, kind) WHERE is_current. +// 2. КПП у ЮЛ не вводится (OQ-31: поле есть только в макете; решение владельца — не нужен). [Отменено в v5.17 — КПП введён.] +// 3. Без изменений структуры, уточнение семантики: company_group.crm_id = «iD ГК» выгрузки ЕЦП CRM +// (docs/source/biq/ecp-export-lagal-entity.xlsx), base_regional_office — «Базовый РФ ГК в CRM»; флаги «входит в ГСК/ГСЗ» +// выгрузки → членство в одной неявной ГСК/ГСЗ на ГК (правила импорта — 05-domain-rules.md §13). +// 4. audit_action: добавлены LEGAL_ENTITY_CHANGE (создание/изменение/закрытие/перевод ЮЛ, алиасы) и USER_CHANGE +// (создание/деактивация пользователя администратором) — ТЗ 8.2 «журнал изменений справочников» их требует, +// в v5.13 были только GROUP_CHANGE / MEMBERSHIP_CHANGE / USER_ROLE_CHANGE. +// +// v5.13 (ревью 30.09–01.10, замечания 1–7; п.2 parent_entry_id — отложен): +// 1. Права как данные: permission + role_permission (матрица доступов ТЗ сноска 29 — в БД, не в коде); +// права только через роли, прямых назначений пользователю нет. Отменяет решение v5.4. +// 3. osv_group_schema убрана — схема группировок шапки = recognition_job.group_schema jsonb; +// тип строки — osv_entry.group_type. +// 4. financial_statement_value.needs_review убран — производное от confidence/is_recognized, считается запросом. +// 5. recognition_job: оставлен только result_s3_object_id; сырой ответ ИС Сканирование в БД не регистрируется +// (scan-gateway пишет его в S3 по конвенции raw/{job_id}.json для разбора инцидентов). +// 6. stored_file.fields_modified убран — COUNT(is_modified) по данным документа. +// 6-бис. Разделение «файл / документ»: stored_file = физический файл (один на загрузку), document = логическая +// единица (ББ, ОФР, ОСВ по счёту, расшифровка) — один файл содержит N документов (PDF с ББ+ОФР, xlsx с +// несколькими ОСВ листами). Чек-лист, проверки периода/контрагента, подтверждение, FK разобранных данных, +// consolidation_document и ex_snapshot_item — на document. Замена (superseded_by) остаётся на файле целиком. +// 7. file_type: убран P7. +// +// Изменения относительно v4 (кратко): +// FIX — исправлены дефекты v4 (PK у meta, типы, копипаст-note про «контрагента», row_type, parent_entry_id) +// ADD — audit_event, status_history, company_group_assignment, elimination_rule, statement_check_result, +// consolidated_osv_line, chess_cell, vgo_pair_match (агрегат по ТЗ 6.4), ofr_vgo_match, kfo_result_*, +// client_kfo_line, comment, export_log/preset, ex_snapshot*, correction_version +// CHG — role → 7 ролей ТЗ + сегмент (ЗО); consolidation_version — сценарий/подход/гудвилл/решение УОБ; +// stored_file — счёт ОСВ, версии/замена; osv_entry — признак контрагента + override; legal_entity — монозаёмщик +// +// Ключи: у ВСЕХ сущностей суррогатный PK (uuid). Натуральные ключи — UNIQUE-колонки. +// (Для высокообъёмных таблиц — osv_entry, audit_event — uuid можно заменить на bigint identity.) +// ЮЛ и ГК — SCD Type 4 (живая таблица + *_history). Членство в ГСК/ГСЗ — SCD Type 2 (is_current). +// Первичка не мутируется: правки распознанных значений = поле + *_original_value + is_modified + audit_event. +// Производные объекты версии (сводная ОСВ, шахматка, КФО) — snapshot на consolidation_version: +// пересчитываются пока версия «в работе», замораживаются при VERIFIED (ТЗ 6.10, 7.2). +// +// DECISION-1 (ТЗ 6.4 vs v4): первичная модель сопоставления ВГО — АГРЕГАТ по паре компаний × правилу пары счетов +// (vgo_pair_match), как в ТЗ/Прил. 3. Строковое 1:1 сопоставление (vgo_match) сохранено как детализация агрегата. +// DECISION-2 (ЕХФД): ЕХ — те же таблицы + ex_snapshot как «замороженный указатель» на состав верифицированной версии. +// Отдельная физическая БД для ЕХ не предполагается; архив в data lake — выгрузка снапшота. +// DECISION-3: ГО — обычная запись regional_office с is_head_office=true (вместо NULL). +// DECISION-5 (производные консолидации): сводная ОСВ, пары ВГО и шахматка НЕ хранятся, пока версия в работе — +// считаются запросом из osv_entry (актуальные прогоны) × consolidation_member × elimination_rule. +// Признак контрагента ГСК/ГК/Внешний — функция от osv_entry.counterparty_id и периметра версии; ручное действие +// аналитика = сопоставление контрагента с ЮЛ (counterparty_id + алиас), комментарий — в audit_event. +// v5.16: способ сопоставления контрагента (osv_entry.counterparty_match_kind) ХРАНИТСЯ; пробелы квартальной склейки и сам свод — нет. +// При VERIFIED производные замораживаются один раз: структурированный снапшот — jsonb в ex_snapshot_item.payload +// (просмотр в ЛК ЕХ), подготовленный xlsx для выгрузки — s3_object. Отдельная NoSQL-база не вводится. +// Корректировки и гудвилл ссылаются на пару ВГО по естественному ключу (правило, компания 1, компания 2). +// DECISION-4 (распознавание): два отдельных stateless-сервиса без доступа к БД — +// tabular-parser (XLS/XLSX/XLSB) и scan-gateway (адаптер ИС Сканирование для PDF/PNG/JPG). +// Запрос: NATS recognition.{tabular|image}.request + файл в S3 uploads/. Результат: JSON в S3 results/ + +// NATS recognition.result. Единственный писатель в БД — arm-core; корреляция по recognition_job.id; +// идемпотентность по msg_id; outbox при создании recognition_job; DLQ. +// v5.13: результат одного файла содержит N документов; ядро создаёт document при маппинге результата. +// +// v5.1 (по макету dkb_24.09.26): consolidation_lock (взятие в работу), participant_disclosure (расшифровки участника), +// legal_entity_history.event_type, statement_meta реквизиты (ОКПО/ОКОПФ/ОКФС/адрес), kfo_result_line: vgo/share, +// client_kfo_line: ВГО клиента/банка + причина, audit_event: section/result/user_agent, статусы REJECTED/ARCHIVED, +// document_check (проверка периода/контрагента файла), company_group.crm_id nullable (ANL-037). +// v5.2 (DECISION-4): recognition_job: subject/msg_id/deadline/external_task_id/result_schema_version/error_code, статусы timeout/dlq. +// v5.12 (ревью part09): ЕХ самодостаточен — срез не ссылается FK на консолидацию и её результаты (их удаляют по сроку, +// срез хранится «вечно»): ex_snapshot.source_version_id/no без FK; ex_snapshot_item = stored_file_id (исходники храним) +// ИЛИ payload jsonb (сводная ОСВ, ВГО, шахматка, КФО, гудвилл, корректировки); убраны object_type/object_id, +// prepared_s3_object_id (готовые файлы не храним), ex_snapshot.source_system, audit_event.s3_object_id; +// добавлен ex_snapshot.superseded_by_snapshot_id (архивный → актуальный). +// v5.11: consolidation_version.elimination_rules_version — какая версия правил Прил. 3/4 применялась (объяснимость и +// воспроизводимость расчёта замороженной версии). +// v5.10 (DECISION-5, ревью part08): убраны consolidated_osv_line, consolidated_osv_source, vgo_pair_match, chess_sheet, chess_cell, +// osv_entry.counterparty_type (выводится); correction ссылается на пару по (rule_id, company_1_id, company_2_id); +// ex_snapshot_item получил payload jsonb. +// v5.9 (ревью part06/07): statement_check_result убрана — проверки считаются на лету, акцепт КА — на документе +// (financial_statement_meta.checks_accepted_*); reviewed_by/at перенесены в meta (документ ревьюится целиком); +// financial_statement_line: убраны ordinal/row_kind/section (порядок и группирующие строки — шаблон формы на фронте); +// consolidation: company_group_id NOT NULL, related_group_id nullable — ГСК фиксируется при VERIFIED (ANL-029); +// consolidation_version: убран package_uuid (документы в ИС Сканирование по одному, ключ = recognition_job.id), добавлен updated_by. +// v5.8 (ревью part01): убраны company_group_assignment (видимость ТЗ 8.1 выводится из company_group.base_regional_office_id, +// legal_entity.regional_office_id, app_user.regional_office_id; история перезакрепления — company_group_history), +// legal_entity.is_mono_borrower (монозаёмщик = company_group_id IS NULL), related_group_member.kind и индекс related_group(id, kind) +// (после ANL-030 partial-unique по legal_entity_id без условия на kind). +// v5.7: метрики качества распознавания — recognition_job.fields_total/low_confidence/unrecognized (что пришло от сервиса). +// (stored_file.fields_modified/fields_reviewed и financial_statement_value.needs_review — убраны в v5.9/v5.13 как производные.) +// v5.6 (ревизия 28.09, решения по каждой убранной таблице): +// account_code_alias — нормализация кодов в коде; редкие субсчета клиентов добавляются в chart_of_account +// vat_rate — константы; средневзвешенная ставка хранится в vgo_pair_match +// crm_import_batch — импорт справочников только на старте (миграция); след в audit_event(DIRECTORY_IMPORT) + файл в s3_object +// osv_entry_edit — audit_event(RECOGNIZED_DATA_EDIT) + osv_entry.original_values/modified_by/modified_at +// document_check — колонки period_check/counterparty_check (с v5.13 — на document); сверка с периодом версии вычисляется +// export_preset — выбор колонок в момент экспорта, без сохранения наборов +// export_log — audit_event(EXPORT, after_value={report_code, format, columns, rows}); файлы выгрузок не хранятся (S3 lifecycle exports/ = 7 дней) +// v5.5: убрана correction_version — история корректировок (сноска 22) ведётся audit_event(before/after), version_no в correction. +// v5.4: возвращён s3_object (реестр объектов S3 — lifecycle/ретеншн ТЗ 8.4, sha256, единая точка ключей); permission/role_permission — матрица в коде (ОТМЕНЕНО в v5.13). +// v5.3 РЕВИЗИЯ по правилу «в базе только то, что есть в ТЗ или на экране макета». Удалено 14 таблиц: +// - account_code_alias: нормализация кодов (60.1=60.01) — алгоритм в коде (ARCH-004), не справочник +// - vat_rate: ставки 0/10/20/22 — константы; средневзвешенная ставка хранится в vgo_pair_match.vat_weighted_rate +// - disclosure_item: расшифровки участника: в UI скрыты, в ТЗ только тип документа «Расшифровка» → вне scope до решения ANL-040 +// - participant_disclosure: см. disclosure_item +// - crm_import_batch: журнал импортов не в ТЗ/UI; факт импорта и ошибки — audit_event(GROUP_CHANGE/IMPORT) +// - permission / role_permission: ВОЗВРАЩЕНЫ в v5.13 (замечание 1) +// - integration_message: РЕШЕНИЕ — обмен с NATS завязан только на recognition_job (status queued→published = outbox; result_msg_id UNIQUE = inbox/дедуп); отдельная таблица появится, только если возникнет второй тип сообщений +// - osv_entry_edit: история правок сумм ОСВ — audit_event(RECOGNIZED_DATA_EDIT, before/after) +// - document_check: результат проверки периода/контрагента — две колонки (с v5.13 — в document) +// - vgo_match: строковое сопоставление 1:1 — только в аналитике, нет ни в ТЗ (агрегат по Прил. 3), ни в UI; детализация агрегата — consolidated_osv_source +// - export_preset: сохранённые пресеты колонок не в ТЗ/UI (выбор столбцов — при экспорте, без сохранения) +// - export_log: история выгрузок (ТЗ 8.2, ЕХ 6.6) — audit_event(EXPORT) с составом в after_value + +// ============ ENUMS ============ +Enum entity_type { + "ЮЛ" + "ИП" +} + +Enum related_group_kind { + gsk + gsz +} + +Enum account_kind { + A [note: 'актив'] + P [note: 'пассив'] + AP [note: 'активно-пассивный'] +} + +Enum account_row_type { + account [note: 'синтетический счёт, напр. 60'] + subaccount [note: 'субсчёт, напр. 60.01'] + analytic [note: 'подсубсчёт, напр. 60.01.1 / 76.07.1'] +} + +Enum business_segment { + DKB [note: 'крупный бизнес'] + DMSB [note: 'малый и средний'] + DM [note: 'микробизнес'] +} + +Enum document_type { + BALANCE [note: 'Бухгалтерский баланс (0710001)'] + INCOME [note: 'ОФР (0710002)'] + OSV [note: 'ОСВ по счёту — osv_account_code обязателен'] + DISCLOSURE [note: 'Расшифровка'] + OTHER [note: 'Прочий документ'] +} + +Enum file_type { + XLS + XLSX + XLSB + PDF + PNG + JPG +} + +Enum recognition_pipeline { + tabular [note: 'XLS/XLSX/XLSB — tabular-parser'] + image [note: 'PDF/PNG/JPG — ИС Сканирование через scan-gateway'] +} + +Enum period_quarter { + Q1 + Q2 + Q3 + Q4 +} + +// v5.13: статус ФАЙЛА — только техническая обработка. Проверка и подтверждение аналитиком — на document. +Enum file_status { + UPLOADED [note: 'Загружен: поступил, в очереди'] + PROCESSING [note: 'Обработка: распознавание/парсинг'] + PROCESSED [note: 'Результат получен, документы созданы'] + ERROR [note: 'Ошибка: нечитаемый файл, требуется повторная загрузка'] + SUPERSEDED [note: 'Заменён новой версией файла (ТЗ 8.2) — вместе со всеми документами в нём'] +} + +// v5.13: статус ДОКУМЕНТА (ANL-003: «Требуется проверка» при confidence < 100% или непройденных проверках; +// «Подтвержден» — финальный после проверки аналитиком) +Enum document_status { + RECOGNIZED [note: 'данные получены, ручная проверка не требуется по порогу'] + NEEDS_CHECK [note: 'Требуется ручная проверка: confidence < 100% / контрольные суммы / период или контрагент не совпали'] + CONFIRMED [note: 'Подтвержден аналитиком: данные зафиксированы, доступны консолидации'] + ERROR [note: 'документ не разобран (напр. не найдена шапка ОСВ)'] +} + +Enum s3_bucket_role { + uploads + results + exports + archive +} + +Enum recognition_status { + queued [note: 'создано, запрос в outbox'] + published [note: 'запрос опубликован в NATS'] + processing [note: 'сервис подтвердил взятие в работу (опц. heartbeat)'] + done + needs_check [note: 'результат получен, но confidence < 100% / контрольные суммы не сошлись'] + error + timeout [note: 'reply_deadline истёк без результата'] + dlq [note: 'сообщение ушло в DLQ после max_deliver'] +} + +Enum statement_type { + balance + income +} + +// v5.19: статус строки формы ББ/ОФР (05-domain-rules.md §11.4) +Enum statement_line_status { + ok [note: 'код разобран'] + unreadable [note: 'в ячейке кода есть цифры, но код не разобран (сырой текст — line_code_text)'] + missing [note: 'строка без кода с содержимым, выше в документе нет строки с кодом'] + detail [note: 'расшифровка без кода под строкой с кодом (Р-8): в §7, агрегаты, КФО и ЕХ не входит'] +} + +// v5.19: вариант формы — по структуре кодов (§11.7) +Enum statement_form_variant { + full + simplified [note: 'упрощённая форма (приказ 66н, прил. 5): ББ без 1100/1200, ОФР без 2100/2200/2300'] +} + +// v5.19: вид записи document_issue +Enum document_issue_kind { + warning [note: 'предупреждение (05-domain-rules.md §11.9, таблица уровней)'] + relation [note: 'жёсткое соотношение §7, не сошедшееся вне допуска и округления'] + soft [note: 'мягкое соотношение или жёсткое в пределах округления (OQ-138)'] +} + +Enum issue_severity { + error [note: 'документ needs_check'] + info [note: 'на статус не влияет'] +} + +Enum issue_source { + service [note: 'пришло в результате распознавания'] + core [note: 'посчитано ядром (проверки по словарю имён и перечню кодов; пересчёт §7 после правки)'] +} + +Enum osv_group_type { + ACCOUNT + DEPARTMENT + COUNTERPARTY + CONTRACT + OTHER +} + +Enum counterparty_match_kind { + INN [note: 'автоматический резолв при маппинге: ИНН строки совпал с legal_entity.inn'] + NAME [note: 'автоматический резолв: точное совпадение имени строки с legal_entity.name'] + ALIAS [note: 'автоматический резолв: нормализованное имя ЮЛ или алиас (legal_entity_alias)'] + MANUAL [note: 'ручное сопоставление аналитиком'] + PROPAGATED [note: 'автоприменение ручного сопоставления к строкам с тем же именем в версии (DOC-221278 п. 12–13)'] +} + +Enum osv_data_type { + BU + NU [note: 'v5.15: налоговый учёт из ОСВ «БУ, НУ» (OQ-45)'] + CURRENCY + QUANTITY + SUM +} + +Enum consolidation_scenario { + CREATE [note: '«Создание КФО» при отсутствии клиентской консолидации'] + CONFIRM [note: '«Подтверждение КФО клиента»'] +} + +Enum consolidation_approach { + BASIC [note: 'базовый: ББ, ОФР, ОСВ 01,58,60,62,66,67,76,90'] + EXTENDED [note: 'опция к базовому: + ОСВ 02,03,08,44,26,91,51,52,59,80,83,86,97,001,008,009,011'] +} + +// Статусы КФО по ТЗ 6.10 + «Корректировка» при расхождении ≥5% (ТЗ 6.4) +Enum consolidation_status { + INITIATED + DOCUMENTS_PROCESSING + DATA_CORRECTION [note: 'корректировка распознанных данных'] + CONSOLIDATION_RF + ADJUSTMENT [note: '«Корректировка» ВГО (расхождение ≥ 5%); подстатус этапа консолидации'] + VERIFICATION_RF + CONSOLIDATION_GO + VERIFICATION_GO [note: 'РКА ГО, если КФО инициировано КА ГО (ТЗ 6.10 маршрут)'] + VERIFIED + CANCELLED + // v5.1 по макету — ANL-035: подтвердить с заказчиком + REJECTED [note: 'Отклонено: возвращено на доработку (макет). Альтернатива — событие в status_history без отдельного статуса'] + ARCHIVED [note: 'Архив: скрыто из основного списка (макет); применимо к VERIFIED/CANCELLED'] +} + +Enum correction_status { + DRAFT + APPLIED + REVOKED +} + +Enum document_check_result { + MATCH + MISMATCH + UNKNOWN +} + +Enum le_history_event { + CREATE + UPDATE + MOVE_TO_GROUP [note: '«Переход в другую ГК»'] + CLOSE [note: '«Закрытие» / вывод из состава ГК'] + REMOVE_FROM_GROUP +} + +Enum ex_snapshot_status { + ACTUAL + ARCHIVE +} + +// v5.13: объекты и действия матрицы прав (ТЗ Прил. 2 «Роль: перечень полномочий», сноска 29) +Enum permission_object { + DIRECTORY [note: 'справочники ГК/ГСК/ГСЗ/ЮЛ'] + DOCUMENT [note: 'первичные документы и распознанные данные'] + CONSOLIDATION [note: 'консолидация, версии, корректировки, гудвилл'] + KFO [note: 'результат КФО: утверждение, верификация'] + EX [note: 'Единое хранилище'] + EXPORT + USERS [note: 'пользователи и роли'] + AUDIT [note: 'журнал действий'] +} + +Enum permission_action { + VIEW + EDIT + APPROVE [note: 'утверждение в РФ'] + VERIFY [note: 'верификация в ГО'] + DELETE + EXPORT + ADMIN +} + +Enum audit_section { + DIRECTORIES + CONSOLIDATION + PRIMARY_DOCUMENTS + RECOGNITION + KFO + EXPORT + ACCESS + SESSION + USERS +} + +Enum audit_action { + LOGIN + LOGIN_FAILED + LOGOUT + CONSOLIDATION_CREATE + CONSOLIDATION_STATUS_CHANGE + CONSOLIDATION_APPROVE + CONSOLIDATION_VERIFY + GROUP_CHANGE + LEGAL_ENTITY_CHANGE [note: 'v5.14: ЮЛ создано/изменено/закрыто/переведено, алиасы'] + MEMBERSHIP_CHANGE + ASSIGNMENT_CHANGE + FILE_UPLOAD + FILE_DELETE + FILE_REPLACE + FILE_REPROCESS [note: 'v5.21: повторная обработка файла — новое recognition_job (attempt + 1), не новая загрузка'] + DOCUMENT_CONFIRM + CONSOLIDATION_LOCK + CONSOLIDATION_LOCK_TAKEOVER + DISCLOSURE_EDIT + RECOGNIZED_DATA_EDIT + OSV_COUNTERPARTY_MAP + OSV_TYPE_OVERRIDE + CORRECTION_CREATE + CORRECTION_EDIT + CORRECTION_REVOKE + GOODWILL_EDIT + KFO_EDIT [note: 'v5.26: ручные поля КФО — комментарии строк, обоснование порога, причины отклонений от Методики; раздел KFO'] + EXPORT + DIRECTORY_IMPORT + USER_ROLE_CHANGE + USER_CHANGE [note: 'v5.14: пользователь создан/изменён/деактивирован администратором'] + ROLE_PERMISSION_CHANGE +} + +// ============ СПРАВОЧНИКИ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (пользователь ГО / пользователь РФ; закрепление ГК за РФ); Прил. 2 «Пользователь: привязка к РФ/ГО» +// Аналитика: md «Модель данных» (regional_office) +// Макет: справочник ГК: колонка «Основной РФ»; пользователи: «Ростовский РФ» и т.п. +// В модели: v4 +// Зачем: справочник филиалов; DECISION-3 — ГО явной записью +Table regional_office { + id uuid [pk] + name varchar [not null, unique] + is_head_office boolean [not null, default: false, note: 'DECISION-3: ГО — явная запись'] + is_active boolean [not null, default: true] + Note: 'Филиал: ГО и региональные филиалы (РФ)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (справочник ГК, импорт из ЕЦП CRM, создание вручную Куратором); Прил. 2 «ГК: id, наименование, id ЕЦП CRM» +// Аналитика: md «Модель данных» (company_group, SCD4) +// Макет: «Группы компаний»: Создать ГК, CRM ID «генерируется автоматически», Архив +// В модели: v4 + v5.1 (code, crm_id nullable — ANL-037) +Table company_group { + id uuid [pk] + code varchar [not null, unique, note: 'v5.1 ANL-037: внутренний код ГК, генерируется системой'] + crm_id varchar [unique, note: 'ключ из ЕЦП CRM; NULL для ГК, созданной вручную Куратором до сверки с CRM'] + name varchar [not null] + base_regional_office_id uuid [ref: > regional_office.id, note: 'закрепление ГК за филиалом (ТЗ 6.1/8.1); перезакрепление Куратором = UPDATE + снимок в history'] + is_archived boolean [not null, default: false, note: 'v5.1 макет: «Архив» ГК'] + valid_from timestamptz [not null] + updated_by uuid [ref: > app_user.id] + created_at timestamptz + updated_at timestamptz + Note: 'ГК — ТЕКУЩЕЕ состояние (SCD4). История — company_group_history' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 «фиксация изменений ГК/ГСК в истории изменений»; 8.2 «изменение состава ГСК, ГК» +// Аналитика: md «Модель данных» (company_group_history) +// Макет: «История изменений» в карточке ГК +// В модели: v4 +// Зачем: прошлые снимки ГК (SCD4) +Table company_group_history { + id uuid [pk] + company_group_id uuid [not null, ref: > company_group.id] + crm_id varchar + name varchar + base_regional_office_id uuid + valid_from timestamptz + valid_to timestamptz + changed_by uuid [ref: > app_user.id] + Indexes { (company_group_id, valid_from) [unique] } +} + + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (ЮЛ/ИП, только одна ГСК/ГСЗ); Прил. 2 «ЮЛ: id, наименование, ИНН, id CRM, ГК, признак»; ЕХ ТЗ 6.1 «Монозаёмщик» +// Аналитика: md «Модель данных» (legal_entity, SCD4) +// Макет: «Юридические лица», «ЮЛ вне ГК» / «Монозаемщики» +// В модели: v4 + v5 (crm_id, is_mono_borrower, ГК nullable); v5.17 — kpp +Table legal_entity { + id uuid [pk] + inn varchar [not null, unique] + crm_id varchar [unique, note: 'ADD: id ЮЛ в ЕЦП CRM (Прил. 2 ТЗ)'] + entity_type entity_type + name varchar [not null] + company_group_id uuid [ref: > company_group.id, note: 'nullable: NULL = монозаёмщик (ЕХ 6.1), признак выводится'] + regional_office_id uuid [ref: > regional_office.id] + close_date date [note: 'NULL = активна'] + valid_from timestamptz [not null] + updated_by uuid [ref: > app_user.id] + created_at timestamptz + updated_at timestamptz + kpp varchar [note: 'v5.17 (добавлена последней колонкой, как её добавляет миграция 0005): КПП, nullable; 9 символов (4 цифры, 2 цифры/латинские A–Z, 3 цифры); у ИП запрещён — проверяет ядро (INVALID_KPP). Источник: макет [50][51-02][61][79], решение владельца 07.10.2026 (OQ-31)'] + Note: 'Организация (ЮЛ/ИП) — ТЕКУЩЕЕ состояние (SCD4). История — legal_entity_history' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.2 (журнал изменений справочников); 6.10 (история изменений состава) +// Аналитика: md «Модель данных» (legal_entity_history) +// Макет: Архив ГК: события «Переход в другую ГК», «Закрытие», дата перевода +// В модели: v4 + v5.1 (event_type, effective_date — ANL-046); v5.17 — kpp +Table legal_entity_history { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id] + inn varchar + crm_id varchar + entity_type entity_type + name varchar + company_group_id uuid + regional_office_id uuid + close_date date + event_type le_history_event [note: 'v5.1 макет: «Переход в другую ГК» / «Закрытие» — для архива ГК'] + effective_date date [note: 'дата перевода/закрытия (не раньше текущей — ANL-046)'] + valid_from timestamptz + valid_to timestamptz + changed_by uuid [ref: > app_user.id] + kpp varchar [note: 'v5.17 (последней колонкой, миграция 0005): снимок КПП на момент события'] + Indexes { (legal_entity_id, valid_from) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.5 «ведение справочника соответствий наименований контрагентов», «использование ранее подтверждённых сопоставлений» +// Аналитика: md «Сводная ОСВ» (алгоритм ручного сопоставления, legal_entity_alias); FNR01 п.9 +// Макет: карточка ЮЛ: «Альтернативные наименования», «Связь с компанией из ОСВ (алиас)» +// В модели: v4 +Table legal_entity_alias { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id] + alias varchar [not null, note: 'как в ОСВ'] + normalized_alias varchar [not null, note: 'lower/trim/без пунктуации/без ОПФ'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + Indexes { + (legal_entity_id, normalized_alias) [unique] + normalized_alias [note: 'ANL-028: конфликт «один алиас у разных ЮЛ» решается при ручном сопоставлении'] + } + Note: 'Словарь соответствий наименований контрагентов (ТЗ 6.5)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (ГСК/ГСЗ в составе ГК; иерархия не поддерживается; в одной ГК несколько ГСК — сноска 11); Прил. 2 «ГСК/ГСЗ» +// Аналитика: md «Модель данных» (related_group, kind) +// Макет: вкладки «ГСК», «ГСЗ», коды GSK-001/GSZ-001 +// В модели: v4; v5.19 — crm_id +Table related_group { + id uuid [pk] + code varchar [not null, unique] + company_group_id uuid [not null, ref: > company_group.id] + kind related_group_kind [not null] + name varchar [not null] + created_date date + crm_id varchar [unique, note: 'v5.19: ID ГСК/ГСЗ в ЕЦП CRM (формы 51-03, 51-04, 77-02; arm-core#112, OQ-33); вводится вручную, импорт ЕЦП не заполняет (OQ-91); NULL — не задан'] + Note: 'ГСК/ГСЗ — current-state. Иерархия групп не поддерживается (ТЗ 6.1)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 «КА может редактировать состав ГСК»; «одно ЮЛ только в одной ГСК или ГСЗ» +// Аналитика: md «Модель данных» (related_group_member, SCD2) +// Макет: «Состав», «Входит в ГСК / ГСЗ», «Редактировать состав» +// В модели: v4 (индекс уточнён — ANL-030); v5.14 — kind и индекс (legal_entity_id, kind) +Table related_group_member { + id uuid [pk] + related_group_id uuid [not null, ref: > related_group.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + kind related_group_kind [not null, note: 'v5.14: денормализация related_group.kind ради частичного уникального индекса; равенство с группой проверяет ядро при записи'] + valid_from timestamptz [not null] + valid_to timestamptz + is_current boolean [not null] + created_by uuid [ref: > app_user.id] + Indexes { + (related_group_id, legal_entity_id) [note: 'partial UNIQUE WHERE is_current'] + (legal_entity_id, kind) [note: 'partial UNIQUE WHERE is_current — v5.14: ЮЛ в одной ГСК И одной ГСЗ одновременно (решение владельца 04.10.2026, отменяет ANL-030); ГК у ЮЛ и у группы должна совпадать (проверяет ядро)'] + } + Note: 'Состав ГСК/ГСЗ — SCD2. Исключение = закрытие версии (valid_to, is_current=false)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2/6.5 (счета и субсчета 01…90, нормализация 60.1=60.01); Прил. 3/4 (пары счетов); сноска 32 +// Аналитика: md «Модель данных» (chart_of_account); md «Шахматка» (kind A/P/AP, row_type) +// Макет: фильтр «Дебетовые/Кредитовые счета», полный план счетов 1С в бандле +// В модели: v4 + v5 (row_type, is_off_balance) +Table chart_of_account { + id uuid [pk] + code varchar [not null, unique, note: 'нормализованный код: 60.01, 76.07.1'] + parent_id uuid [ref: > chart_of_account.id] + name varchar [not null] + kind account_kind + row_type account_row_type [not null, note: 'FIX: используется в «Шахматке»'] + is_off_balance boolean [not null, default: false, note: '001, 008, 009, 011'] + Note: 'План счетов' +} + + +// ADD ТЗ Прил. 3/4 как данные (сноски 32/35: пары могут корректироваться на этапе реализации) +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 + Прил. 3 «Перечень статей ББ» и Прил. 4 «Модель расчётов ОФР»; сноски 32/35 «могут быть скорректированы на этапе реализации» +// Аналитика: md «Автоматическое сопоставление ВГО» (таблица «Правила встречных счетов» — хардкод) +// Макет: — +// В модели: v5 новая +// Зачем: правила элиминации как данные, версионируемые +Table elimination_rule { + id uuid [pk] + statement_type statement_type [not null, note: 'balance = Прил. 3, income = Прил. 4'] + rule_no smallint [not null, note: '№ строки приложения'] + article_1_line varchar [not null, note: 'статья Компании 1, код строки ББ/ОФР, напр. 1230'] + article_2_line varchar [not null, note: 'статья Компании 2, напр. 1520'] + side_1 varchar [not null, note: 'D | K — сторона Компании 1'] + measure varchar [not null, note: 'closing | turnover — сальдо на конец (ББ) или оборот (ОФР)'] + elimination_mode varchar [not null, note: 'min | min_or_full | full | manual — min(A,B) в допуске / v5.26: min(A,B) при обеих сторонах, иначе A, без порога (ББ 2, DOC-274230) / вся сумма ВГО (ОФР выручка) / вручную'] + vat_handling varchar [note: 'none | check_90_01_vs_2110 | weighted_rate — только для ОФР'] + excise_adjust boolean [not null, default: false] + comment text + version_no int [not null] + valid_from date [not null] + valid_to date + Indexes { (statement_type, rule_no, version_no) [unique] } + Note: 'Правило элиминации. Состав счетов — в elimination_rule_account' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 3/4 (столбцы «Пары счетов Компании 1 и Компании 2», исключения «76 кроме 76.7.1») +// Аналитика: md «Автоматическое сопоставление ВГО» +// Макет: — +// В модели: v5 новая +// Зачем: состав счетов сторон правила +Table elimination_rule_account { + id uuid [pk] + rule_id uuid [not null, ref: > elimination_rule.id] + company_no smallint [not null, note: '1 или 2'] + account_id uuid [not null, ref: > chart_of_account.id] + is_excluded boolean [not null, default: false, note: 'true = исключение внутри маски (напр. «76 кроме 76.7.1»)'] + Note: 'Счета/субсчета сторон правила' +} + + +// ============ ПОЛЬЗОВАТЕЛИ И ДОСТУП ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (роли, ГО/РФ, ЗО); Прил. 2 «Пользователь: ФИО, роль, привязка к РФ/ГО, ЗО, табельный номер»; РАИСА +// Аналитика: md «Аутентификация и авторизация» (users: email, role, JWT РАИСА); md «Модель данных» (app_user) +// Макет: «Пользователи»: Логин, Роль, Активен, Деактивация +// В модели: v4 + v5 (employee_no, business_segment) +Table app_user { + id uuid [pk] + external_id varchar [unique, note: 'субъект РАИСА/SSO; аутентификация внешняя (ANL-018)'] + email varchar [unique, note: 'v5.15: хранится в нижнем регистре (нормализация при записи), сравнение по lower(email) (OQ-59)'] + full_name varchar [not null] + employee_no varchar [note: 'ADD: табельный номер (Прил. 2 ТЗ)'] + regional_office_id uuid [not null, ref: > regional_office.id, note: 'ГО или РФ (DECISION-3)'] + business_segment business_segment [note: 'ADD: зона ответственности ДКБ/ДМСБ/ДМ (ТЗ 8.1)'] + is_active boolean [not null, default: true] + created_at timestamptz + updated_at timestamptz +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 таблица ролей (КА РФ, КА ГО, РКА, КМ, Куратор, Эксперт, Администратор); Прил. 2 «Роль» +// Аналитика: md «Ролевая модель»; md «Модель данных» (role.code) +// Макет: «Изменение ролей и прав доступа» (Аналитик ДКБ, Наблюдатель… — расходится с ТЗ, ANL-034) +// В модели: v4 (коды → ТЗ в v5); v5.13 — матрица полномочий в role_permission +Table role { + id uuid [pk] + code varchar [not null, unique, note: 'ТЗ 8.1: KA_RF | KA_GO | RKA | CLIENT_MANAGER | CURATOR_GO | EXPERT | ADMIN. Seed; администратор может заводить дополнительные роли (замена исключениям по пользователю)'] + name varchar [not null] + description text + is_system boolean [not null, default: false, note: 'роли ТЗ 8.1 — не удаляются'] + is_active boolean [not null, default: true] + Note: 'Роль. Полномочия — через role_permission (v5.13); код роли в бизнес-логике больше не проверяется' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Роль: перечень полномочий»; сноска 29 «матрица доступов будет разработана» +// Аналитика: md «Ролевая модель» +// Макет: карточка роли: «Права: Просмотр, редактирование КФО / справочники / консолидации» +// В модели: v5 новая, убрана в v5.3/v5.4, ВОЗВРАЩЕНА в v5.13 (замечание 1) +// Зачем: атомарное право «объект × действие»; seed, администратор новые коды не создаёт +Table permission { + id uuid [pk] + code varchar [not null, unique, note: 'напр. CONSOLIDATION_EDIT, KFO_VERIFY, DIRECTORY_EDIT, USERS_ADMIN'] + object_type permission_object [not null] + action permission_action [not null] + name varchar [not null] + description text + Indexes { (object_type, action) [unique] } + Note: 'Справочник прав (seed, привязан к коду). Ограничения по ЗО/филиалу и статусу КФО — отдельный policy-слой поверх прав: право говорит «может», policy — «эту ли и сейчас ли»' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Роль: перечень полномочий» +// Аналитика: нет +// Макет: «Права» в карточке роли, «Изменение ролей и прав доступа» +// В модели: v5.13 (возвращена) +// Зачем: матрица доступов как данные; редактируется администратором, история — audit_event(ROLE_PERMISSION_CHANGE) +Table role_permission { + role_id uuid [not null, ref: > role.id] + permission_id uuid [not null, ref: > permission.id] + granted_by uuid [ref: > app_user.id] + granted_at timestamptz + Indexes { (role_id, permission_id) [pk] } + Note: 'effective_permissions(user) = ∪ permissions(roles(user)). Прямых прав у пользователя нет (решение 30.09): исключения — через заведение роли' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (роль пользователя); Прил. 2 «Пользователь — Роль: многие-к-одному» +// Аналитика: md «Модель данных» v4 (user_role M:N) +// Макет: «Роль» пользователя +// В модели: v4 +// Зачем: M:N оставлен (пользователь может быть КА и Куратором); если ТЗ строго 1:1 — заменить на app_user.role_id +Table user_role { + id uuid [pk] + user_id uuid [not null, ref: > app_user.id] + role_id uuid [not null, ref: > role.id] + Indexes { (user_id, role_id) [unique] } + Note: 'M:N current-state. История назначений — audit_event(USER_ROLE_CHANGE)' +} + +// ============ ФАЙЛЫ / РАСПОЗНАВАНИЕ ============ + +// ИСТОЧНИК: +// ТЗ: 6.2 (файлы с префиксом UUID, исходные документы хранятся); 8.4 «хранение 5 лет с даты прекращения отношений, архивирование/удаление»; ЕХ 6.2 «ссылка на исходные документы с возможностью скачать», ЕХ 8.3 «архивирование в data lake» +// Аналитика: md «Модель данных» v4 (s3_key, raw_result_s3_key — строки) +// Макет: — +// В модели: v5.2 новая (DECISION-4), подтверждена в v5.4 +// Зачем: единая точка ключей всех бакетов для lifecycle/ретеншна и дедупа; без реестра ключи размазаны по 4 таблицам +Table s3_object { + id uuid [pk] + bucket_role s3_bucket_role [not null] + bucket varchar [not null] + key varchar [not null, note: 'схема ключа: {file_id}/{job_id}/{name}'] + content_type varchar + size_bytes bigint + sha256 varchar + version_id varchar [note: 'S3 object version при включённом versioning'] + created_by varchar [note: 'arm-core | tabular-parser | scan-gateway | user:{id}'] + created_at timestamptz [not null] + expires_at timestamptz [note: 'lifecycle: NULL = весь срок (ТЗ 8.4); exports/ = 7 дней; results/ — решение ARCH-010'] + archived_to varchar [note: 'ссылка в data lake после архивирования (ЕХ 8.3)'] + Indexes { (bucket, key) [unique] } + Note: 'Реестр объектов S3. stored_file / recognition_job ссылаются сюда FK' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (типы и форматы документов, UUID); 8.2 «первичные документы не редактируются, только замена с версионностью» +// Аналитика: md «Загрузка документов» (панель, UC-01…05); FNR01 «Маппинг stored_file» +// Макет: «Первичные документы», «Загрузка документов», «Замена документа», «Удаление документа» +// В модели: v4 + v5 (version_no, superseded_by) + v5.2 (s3_object_id); v5.13 — только ФИЗИЧЕСКИЙ файл, документы → document; v5.18 — consolidation_id +Table stored_file { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'организация-владелец (выбрана при загрузке)'] + file_type file_type [not null] + original_name varchar + s3_object_id uuid [not null, ref: > s3_object.id, note: 'исходный файл в uploads'] + content_hash varchar [note: 'sha256, дедуп (UC-01 A2)'] + size_bytes bigint + status file_status [not null] + version_no int [not null, default: 1, note: 'ADD ТЗ 8.2: версия при замене'] + superseded_by_file_id uuid [ref: > stored_file.id, note: 'ссылка на файл-замену; статус SUPERSEDED. Замена — файл ЦЕЛИКОМ со всеми документами в нём (решение 01.10)'] + current_recognition_job_id uuid [ref: > recognition_job.id, note: 'авторитетный прогон; его результат породил актуальные document'] + uploaded_by uuid [ref: > app_user.id] + uploaded_at timestamptz + updated_at timestamptz + consolidation_id uuid [ref: > consolidation.id, note: 'v5.18: консолидация (период ГК), в контексте которой загружен файл — основа «рабочего стола консоли» (полный состав — context/05-domain-rules.md §9.5 п. 11), общего для версий периода; NULL — без контекста. Пишется при загрузке; при автозамене файл без контекста наследует значение заменённого'] + Indexes { + (legal_entity_id, status) + (legal_entity_id, consolidation_id) [note: 'v5.18: основа рабочего стола консоли для ЮЛ (§9.5 п. 11)'] + current_recognition_job_id [note: 'v5.15: проверка FK при удалении recognition_job / файла (OQ-117)'] + } + Note: 'v5.13: физический файл одной загрузки (S3 — только ссылка). Что внутри — document (1:N). Первичка не редактируется. Удаление физическое запрещено после INITIATED — только SUPERSEDED (ANL-020). v5.18: consolidation_id — где загружен; что использует версия — consolidation_document' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (типы документов: ББ, ОФР, ОСВ по счетам; обязательный набор); 5.1 (подход); 8.2; Прил. 2 «Документ ФО» +// Аналитика: md «Загрузка документов» (чек-лист, UC-01 A1 период, «Подтвердить данные»); md «Парсинг ОСВ» (счёт/период из шапки) +// Макет: «Чек-лист загруженных документов», «Соответствует период», «Не соответствует контрагент», «Подтвердить данные» +// В модели: v5.13 новая (замечание 6-бис); v5.19 — file_ordinal, уникальность типа только для не-форм; +// v5.20 — is_modified, original_values, modified_by/at (уточнение периода и счёта ОСВ, OQ-161) +// Зачем: один файл = N документов (PDF ББ+ОФР; xlsx с несколькими ОСВ). Чек-лист, проверки, подтверждение и +// FK разобранных данных — здесь; без этого «ОСВ 58 загружена?» по файлу не ответить +Table document { + id uuid [pk] + stored_file_id uuid [not null, ref: > stored_file.id] + legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'денормализация stored_file.legal_entity_id — выборки чек-листа по ЮЛ+период'] + document_type document_type [not null] + osv_account_code varchar [note: 'для OSV — счёт по шапке (01, 60, 62…), нормализованный; NULL для прочих типов'] + period_year smallint [note: 'из шапки/формы'] + period_quarter period_quarter + is_cumulative boolean [note: 'true = нарастающий итог с начала года'] + reporting_date date [note: 'отчётная дата, если определена'] + locator varchar [note: 'где в файле: лист xlsx / диапазон страниц PDF (напр. «sheet:ОСВ 58», «pages:1-2»)'] + status document_status [not null] + period_check document_check_result [note: 'UC-01 A1 / макет «Соответствует период»'] + counterparty_check document_check_result [note: 'макет «Не соответствует контрагент» — ИНН/наименование из шапки vs legal_entity'] + confirmed_by uuid [ref: > app_user.id, note: 'кто нажал «Подтвердить данные» → audit_event(DOCUMENT_CONFIRM)'] + confirmed_at timestamptz + created_at timestamptz + updated_at timestamptz + file_ordinal smallint [not null, default: 0, note: 'v5.19: индекс документа в documents[] результата распознавания (с 0), у всех документов при любом MAJOR контракта; несозданный документ номер не занимает (номера с пропусками); различает две формы одного типа в файле (Р-10)'] + is_modified boolean [not null, default: false, note: 'v5.20: период или счёт ОСВ уточнены аналитиком и отличаются от распознанных (возврат к исходному снимает признак)'] + original_values jsonb [note: 'v5.20: как распознано, до первой правки — {period_year, period_quarter, is_cumulative, osv_account_code}; NULL = не правилось; после первой правки не меняется'] + modified_by uuid [ref: > app_user.id, note: 'v5.20: кто последним уточнил период или счёт'] + modified_at timestamptz + Indexes { + (stored_file_id, document_type, osv_account_code) [note: 'partial UNIQUE WHERE document_type NOT IN (\'BALANCE\', \'INCOME\') — NULLS NOT DISTINCT; v5.19: одна ОСВ по счёту / одна расшифровка / один прочий на файл; формы ББ/ОФР одного типа в файле могут повторяться (Р-10)'] + (stored_file_id, file_ordinal) [unique, note: 'v5.19: номер документа уникален в файле'] + (legal_entity_id, document_type, osv_account_code, period_year, period_quarter, status) [note: 'чек-лист required_document_rule'] + } + Note: 'Логический документ внутри файла. Создаётся ядром при маппинге результата recognition_job. Разобранные данные: OSV → osv_entry; BALANCE/INCOME → financial_statement_meta. Счётчики «исправлено полей» — COUNT(is_modified) по данным, не хранятся. v5.19: предупреждения и нарушения §7 — document_issue; две формы одного типа и периода в файле не автоподтверждаются (Р-18). v5.20: период и счёт ОСВ уточняются аналитиком на месте, распознанное — в original_values, + audit_event(RECOGNIZED_DATA_EDIT, before/after); уточнение подтверждённого документа сбрасывает подтверждение и снимает привязки к версиям (05-domain-rules §9.5 п. 12)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (статусы процесса распознавания, ИС Сканирование); 6.10 «повторная обработка»; 7.1 +// Аналитика: FNR01 «Маппинг recognition_job»; md «Загрузка документов» UC-02; md «Парсинг ОСВ» (схема группировок шапки) +// Макет: «Система распознавания: получен ответ», «Обработка», «Ошибка» +// В модели: v4, переписана в v5.2 под контракт NATS (DECISION-4); v5.13 — group_schema jsonb, убран raw_result +Table recognition_job { + id uuid [pk, note: 'job_id в контракте NATS'] + file_id uuid [not null, ref: > stored_file.id] + pipeline recognition_pipeline [not null, note: 'tabular → tabular-parser; image → scan-gateway'] + request_subject varchar [not null, note: 'recognition.tabular.request | recognition.image.request'] + request_msg_id varchar [unique, note: 'Nats-Msg-Id запроса (outbox)'] + result_msg_id varchar [unique, note: 'Nats-Msg-Id результата (inbox, дедуп)'] + schema_version varchar [not null, note: 'версия контракта сообщений'] + service_name varchar [note: 'tabular-parser | scan-gateway — из результата'] + service_version varchar + status recognition_status [not null] + attempt smallint [not null, default: 1] + max_attempts smallint [not null, default: 1, note: 'OCR: 1 (0 повторов), парсер: 3'] + requested_at timestamptz + published_at timestamptz + started_at timestamptz + finished_at timestamptz + reply_deadline timestamptz [note: 'таймаут ожидания (OCR — 20 мин)'] + external_task_id varchar [note: 'id задания в ИС Сканирование (только image)'] + error_code varchar [note: 'UNSUPPORTED_FORMAT | UNREADABLE | TIMEOUT | SCHEMA_MISMATCH | …'] + error_message text + result_s3_object_id uuid [ref: > s3_object.id, note: 'нормализованный JSON в results — единственная ссылка на результат (v5.13). Сырой ответ ИС Сканирование — S3 raw/{job_id}.json по конвенции, в БД не регистрируется'] + result_schema_version varchar [note: 'версия схемы JSON результата (ARCH-008/009)'] + group_schema jsonb [note: 'v5.13 (вместо osv_group_schema): схема группировок шапки ОСВ по листам — [{\"locator\":\"sheet:ОСВ 58\",\"groups\":[{\"type\":\"ACCOUNT\"},{\"type\":\"COUNTERPARTY\",\"has_inn\":true},{\"type\":\"CONTRACT\"}]}]; только для tabular'] + metrics jsonb [note: 'duration_ms, pages, rows, warnings (v5.19 / контракт 2.0: предупреждения уровня файла — структуры {code, severity, message, locator?}; error уровня файла блокирует RECOGNIZED документов файла)'] + fields_total int [note: 'v5.7: всего извлечённых значений (ячейки формы / строки ОСВ)'] + fields_low_confidence int [note: 'v5.7: значений с confidence < 100 (ТЗ 6.2) — заполняется при маппинге результата'] + fields_unrecognized int [note: 'v5.7: значений с is_recognized = false'] + statements_version varchar [note: 'v5.19: service.statements_version результата — коммит копии общего кода arm_statements; при пересчёте после правки сравнивается с копией ядра (расхождение — document_issue info)'] + Indexes { (file_id, attempt) [unique] } + Note: 'РЕШЕНИЕ v5.4: единственная точка обмена с NATS. Запись = outbox: создаётся в queued в одной транзакции с stored_file, фоновый публикатор переводит в published (при сбое публикации — повтор по queued старше N сек); результат приходит по NATS, дедуп по result_msg_id UNIQUE; redelivery после max_deliver → status=dlq, replay = сброс в queued с attempt+1. Ядро маппит JSON из S3 в document → osv_entry / financial_statement_*' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (Osv_report_lines); 6.5 (строка ОСВ: компания, счёт, контрагент, признак, 6 сумм); Прил. 2 «Строка сводной ОСВ», «Распознанный показатель» +// Аналитика: FNR01 «Маппинг osv_entry»; md «Парсинг ОСВ» п.11 (row_number, level, group_type, data_type) +// Макет: «Оборотно-сальдовая ведомость»: Дебет/Кредит начало/оборот/конец, «Тип контрагента» +// В модели: v4 + v5 (owner_legal_entity_id — FIX, parent_entry_id, is_modified, is_excisable); v5.13 — document_id, group_type вместо group_schema_id; v5.16 — counterparty_match_kind +Table osv_entry { + id uuid [pk] + recognition_job_id uuid [not null, ref: > recognition_job.id] + document_id uuid [not null, ref: > document.id, note: 'v5.13: ОСВ-документ (счёт + период); файл — через document.stored_file_id'] + owner_legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'FIX: организация-ВЛАДЕЛЕЦ ОСВ (денормализация). В v4 называлось legal_entity_id с ошибочной note'] + parent_entry_id uuid [ref: > osv_entry.id, note: 'родитель в дереве строк ОСВ (ТЗ 6.5). ОТКРЫТО (замечание 2, отложено 01.10): хранить ли только контрагентские строки и нужно ли дерево'] + group_type osv_group_type [note: 'v5.13: тип строки по схеме шапки (recognition_job.group_schema) — ACCOUNT / DEPARTMENT / COUNTERPARTY / CONTRACT / OTHER'] + group_name varchar [note: 'исходное название группировки из шапки («Контрагенты», «Договоры»)'] + row_number int + level smallint + row_value varchar [note: 'исходный текст строки'] + data_type osv_data_type + account_code varchar [note: 'сырой код счёта'] + account_id uuid [ref: > chart_of_account.id] + account_root varchar + counterparty_name varchar [note: 'сырое имя из ОСВ, не изменяется'] + counterparty_inn varchar [note: 'сырой ИНН, не изменяется'] + counterparty_id uuid [ref: > legal_entity.id, note: 'резолв по ИНН/имени/алиасу или ручное сопоставление; NULL = не сопоставлен. Признак ГСК/ГК/Внешний выводится из периметра версии (DECISION-5)'] + opening_debit numeric(18,2) + opening_credit numeric(18,2) + turnover_debit numeric(18,2) + turnover_credit numeric(18,2) + closing_debit numeric(18,2) + closing_credit numeric(18,2) + is_modified boolean [not null, default: false, note: 'значения правились аналитиком'] + original_values jsonb [note: 'v5.6: суммы до первой ручной правки {opening_debit,…}; NULL = не правилось'] + modified_by uuid [ref: > app_user.id] + modified_at timestamptz + is_excisable boolean [note: 'ADD ТЗ 6.4: признак «подакцизный товар» для ОСВ 90'] + counterparty_match_kind counterparty_match_kind [note: 'v5.16: NULL ⇔ counterparty_id NULL; INN/NAME/ALIAS — шаг автоматического резолва при маппинге; MANUAL — ручное сопоставление; PROPAGATED — автоприменение по имени в версии (DOC-221278 п. 12–13)'] + Indexes { + (document_id, row_number) [unique] + (owner_legal_entity_id, account_id, counterparty_id) + parent_entry_id [note: 'v5.15: обход дерева строк и проверка self-FK при удалении документа (OQ-117)'] + recognition_job_id [note: 'v5.15: проверка FK при удалении recognition_job / файла (OQ-117)'] + } + Note: 'Строка ОСВ. Правки сумм аналитиком — original_values/modified_by + audit_event(RECOGNIZED_DATA_EDIT, before/after)' +} + + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 «извлекаются значения по кодам форм Приказа 157н»; 6.3 проверки единиц измерения/даты +// Аналитика: FNR01 «fin_report_header» (report_type, org, unit, currency, reporting_date); md «Модель данных» v4 +// Макет: «Реквизиты организации» (ОКПО, ОКОПФ, ОКФС, ОКУД, ОКЕИ, адрес), «Единица измерения», «Отчетная дата» +// В модели: v4 (FIX pk) + v5.1 (реквизиты — ANL-044); v5.13 — document_id; v5.19 — form_variant, needs_review_fields +Table financial_statement_meta { + id uuid [pk, note: 'FIX: был без PK'] + document_id uuid [not null, unique, ref: > document.id, note: 'v5.13: документ BALANCE/INCOME (1:1); файл — через document'] + recognition_job_id uuid [not null, ref: > recognition_job.id] + statement_type statement_type [not null] + inn varchar + inn_original_value varchar + inn_confidence numeric(5,2) + inn_verified boolean + inn_modified boolean + organization_name varchar [note: 'как в документе; сверяется с legal_entity/alias, при несовпадении — document.counterparty_check'] + report_date date + report_date_original_value varchar [note: 'FIX: строка как распознано (был `date []`)'] + report_date_verified boolean + report_date_modified boolean + units varchar [note: 'тыс. руб. / руб.'] + units_original_value varchar + units_verified boolean + units_modified boolean + currency varchar [default: 'RUB'] + okpo varchar [note: 'v5.1 ANL-044: реквизиты по макету экрана распознавания'] + okopf varchar + okfs varchar + okud varchar [note: '0710001 / 0710002'] + okei varchar + legal_form varchar [note: 'ОПФ'] + ownership_form varchar + address varchar + subject_to_audit boolean + reviewed_by uuid [ref: > app_user.id, note: 'v5.9: кто проверил данные формы; подтверждение документа — document.confirmed_by'] + reviewed_at timestamptz + checks_accepted_by uuid [ref: > app_user.id, note: 'v5.9 ТЗ 6.3 п.7: акцепт КА непройденных проверок (напр. 1370/2400) с комментарием'] + checks_accepted_at timestamptz + checks_accepted_comment text + form_variant statement_form_variant [note: 'v5.19: full | simplified — по структуре кодов (§11.7); выбирает набор соотношений §7; NULL — документ до v5.19 или результат 1.x: набор полный'] + needs_review_fields jsonb [note: 'v5.19: имена полей шапки, требующих проверки (["report_date"]), как пришло от сервиса; правка поля или подтверждение тем же значением убирает его из списка'] + header_source jsonb [note: 'v5.19: шапка результата как пришла — {"<поле>": {"text": <поле>_text, "confidence": <поле>_confidence}} для organization_name, inn, report_date, units, okud, okpo, okopf, okfs, okei, address; не меняется. *_original_value — как прежде: inn_original_value = inn, report_date_original_value = report_date_text, units_original_value = units_text; inn_confidence = header_source.inn.confidence'] + organization_name_original_value varchar [note: 'v5.19: наименование организации из результата; правками не меняется'] + organization_name_modified boolean [note: 'v5.19: наименование правилось аналитиком (PATCH шапки)'] + Note: 'Шапка распознанной формы ББ/ОФР. v5.19: результат проверок §7 и предупреждения хранятся в document_issue как пришли от сервиса и пересчитываются ядром только после правок (пересматривает «не хранятся — считаются на лету»)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 п.7 «стр. 1370 на конец и на начало года»; 6.7 «УК ДО на отчётную дату» +// Аналитика: FNR01 «rows[0].cells[2..4] — наименования колонок периода»; md «Логические проверки» («по каждой отчётной дате (колонке)») +// Макет: колонки «На 30 сентября 2025 г.», «На 31 декабря 2024 г.», «Выбор периодов» +// В модели: v5 новая (FIX-14) +// Зачем: дата колонки — без неё межформенная проверка и гудвилл не считаются +Table financial_statement_column { + id uuid [pk] + meta_id uuid [not null, ref: > financial_statement_meta.id] + column_index smallint [not null] + label varchar [note: 'как в форме: «На 30 сентября 2025 г.», «За 9 месяцев 2025 г.»'] + period_date date [note: 'ADD FIX-14: дата колонки — нужна для 1370(нач.года), гудвилла «на отчётную дату», сравнения версий'] + is_current boolean [note: 'колонка отчётного периода'] + period_kind varchar [note: 'v5.19: absolute | relative:current | relative:prev_year_end | relative:prev_prev_year_end | relative:prev_period | none; относительные подписи бланка 66н — period_date от эффективной даты документа, ядро досчитывает после правки даты отчёта (Р-6)'] + label_confidence numeric(5,2) [note: 'v5.19: уверенность подписи колонки (Excel — 100; подписи нет — NULL)'] + Indexes { (meta_id, column_index) [unique] } + Note: 'Колонки-периоды формы (2–3 сравнительные даты)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (коды статей по формам 157н); 6.3 (Таблица 1 — коды строк 1100…2400) +// Аналитика: FNR01 «fin_report_rows» (row_name, row_code, section, is_total_row); md «Модель данных» v4 +// Макет: «Результат распознавания»: Наименование показателя, Код, разделы +// В модели: v4 (FIX: убраны копипаст-поля); v5.19 — строки без кода и повторы кода, статусы, порядок, needs_review +Table financial_statement_line { + id uuid [pk] + meta_id uuid [not null, ref: > financial_statement_meta.id] + line_code varchar [note: 'код строки ОКУД, напр. 1230'] + line_code_original_value varchar [note: 'v5.19: line_code строки из результата как пришёл (NULL у unreadable / missing / detail и у строк, созданных правкой); правками кода не меняется. До v5.19 NULL был признаком «создана правкой» — теперь это row_index IS NULL'] + line_code_confidence numeric(5,2) + line_code_modified boolean + line_name varchar + line_name_original_value varchar + line_name_confidence numeric(5,2) + line_name_modified boolean [note: 'FIX: был numeric'] + row_index int [note: 'v5.19: порядок строки в документе источника (с 0). NULL ⇔ строка создана правкой аналитика (шаблонная, origin = template); у строк из результата — всегда не NULL (маппер 1.x и 2.0)'] + source_ref varchar [note: 'v5.19: положение в источнике (row:8, row:14#2, Баланс!C27); NULL у строк, созданных правкой'] + line_status statement_line_status [not null, default: 'ok', note: 'v5.19: ok | unreadable | missing | detail; line_code пуст ⇔ статус не ok'] + line_code_text varchar [note: 'v5.19: сырой текст ячейки кода, как распознано'] + parent_row_index int [note: 'v5.19: row_index строки-родителя у detail; пересчитывается при правке'] + needs_review boolean [not null, default: false, note: 'v5.19: неуверенный код не подтверждён структурой (как пришло от сервиса; правка или подтверждение кода — false)'] + is_confirmed boolean [not null, default: false, note: 'v5.19: код подтверждён аналитиком без изменения (PATCH lines confirmed)'] + Indexes { + (meta_id, line_code) [note: 'v5.19: не уникален — повтор кода законен (Р-4); чтения «по коду» — агрегат формы по коду'] + (meta_id, source_ref) [note: 'partial UNIQUE WHERE source_ref IS NOT NULL — v5.19: положение в источнике уникально в форме'] + } + Note: 'Строка формы как в источнике (v5.19): строки с кодом, без кода (unreadable / missing / detail) и повторы кода; раскладка экрана по коду и шаблон формы — на фронте и в ядре по словарю имён' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 «уверенность распознавания ниже 100% → Требуется ручная проверка»; Прил. 2 «Распознанный показатель: значение, уверенность, признак ручной корректировки» +// Аналитика: FNR01 (value_current/prior/prior2, confidence, скобки → отрицательное); md «Логические проверки» (правка ячеек) +// Макет: подсветка ячеек, «не распознано», «Редактировать/Сохранить», «Подтвердить данные» +// В модели: v4 + v5 (column_id, confidence, original_amount, modified_by); v5.13 — убран needs_review; v5.19 — needs_review хранится, source_text, is_confirmed +Table financial_statement_value { + id uuid [pk] + line_id uuid [not null, ref: > financial_statement_line.id] + column_id uuid [not null, ref: > financial_statement_column.id] + amount numeric(18,2) + string_value varchar [note: 'как распознано, если не число'] + confidence numeric(5,2) [note: 'уверенность распознавания (Прил. 2 ТЗ). v5.19: подсветка — только needs_review (уверенность, is_recognized и §7 сведены сервисом)'] + is_recognized boolean + is_modified boolean [not null, default: false] + original_amount numeric(18,2) [note: 'значение до ручной правки'] + modified_by uuid [ref: > app_user.id] + modified_at timestamptz + source_text varchar [note: 'v5.19: сырой текст ячейки источника (values[].text), не меняется'] + needs_review boolean [not null, default: false, note: 'v5.19: как пришло от сервиса (подтверждение проверкой §7, 05-domain-rules.md §11.9), пересчитывается ядром после правок; is_modified или is_confirmed ⇒ false. Пересматривает п. 4 v5.13'] + is_confirmed boolean [not null, default: false, note: 'v5.19: подтверждено аналитиком без изменения значения (is_modified не ставится)'] + confirmed_by uuid [ref: > app_user.id] + confirmed_at timestamptz + Indexes { (line_id, column_id) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 (логические проверки, акцепт); 6.2 «уверенность ниже 100% → Требуется ручная проверка» +// Аналитика: openspec/changes/archive/2026-10-19-unified-statement-normalization (design: шаг 8, «Что уходит из ядра») +// Макет: «Результат распознавания»: ошибки проверок, подсветка ячеек +// В модели: v5.19 новая +// Зачем: предупреждения сервиса и нарушения §7 с привязкой к строкам, ячейкам и полям шапки — экран показывает их у +// места и после сохранения; ядро не пересчитывает их при загрузке (Decisions № 17, 19) +Table document_issue { + id uuid [pk] + document_id uuid [not null, ref: > document.id] + kind document_issue_kind [not null] + code varchar [not null, note: 'код предупреждения (05-domain-rules.md §11.9); у relation — RELATION_FAILED (severity error), у soft — RELATION_SOFT (severity info)'] + severity issue_severity [not null] + message text [not null] + line_ids jsonb [note: 'массив financial_statement_line.id, к которым относится запись (нарушение — все строки кодов соотношения); FK нет — при удалении шаблонной строки (назначение её кода строке источника) ядро убирает её id из line_ids всех записей документа'] + column_id uuid [ref: > financial_statement_column.id] + column_index smallint [note: 'столбец сетки из предупреждения, если колонки нет среди financial_statement_column (COLUMN_SKIPPED и др.); у колонки формы — column_id'] + codes jsonb [note: 'для relation / soft: коды цели и членов соотношения (["1600","1100","1200"]); экран подсвечивает строки этих кодов'] + field varchar [note: 'поле шапки (report_date, inn, organization_name, …)'] + source issue_source [not null] + relation varchar [note: 'для relation / soft: текст соотношения (1600 = 1100 + 1200)'] + expected numeric(18,2) [note: 'для relation / soft: цель'] + actual numeric(18,2) [note: 'для relation / soft: правая часть'] + created_at timestamptz [not null] + Indexes { document_id } + Note: 'v5.19: предупреждения и нарушения документа (формы — предупреждения и нарушения §7; ОСВ — предупреждения листа OSV_* результата 2.0). Удаляются вместе с документом при перезагрузке файла; при пересчёте после правки relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой (таблица §11.9)' +} + + +// ============ КОНСОЛИДАЦИЯ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 (процесс формирования КФО за отчётный период); 7.2 «формируется по требованию»; Прил. 2 «Консолидация: ГСК, отчётный период» +// Аналитика: md «Сводная ОСВ» / «Шахматка» (consolidation.year/quarter); md «Модель данных» v4 +// Макет: «Создание консолидации: ГК → ГСК → Квартал» +// В модели: v4 + v5 (reporting_date) +// Зачем: логическая консолидация ГСК+период; версии — ниже +Table consolidation { + id uuid [pk] + company_group_id uuid [not null, ref: > company_group.id, note: 'v5.9: консолидация создаётся от ГК'] + related_group_id uuid [ref: > related_group.id, note: 'v5.9: ГСК — NULL до утверждения; фиксируется/создаётся при VERIFIED по составу consolidation_member (ANL-029)'] + year smallint [not null] + quarter period_quarter [not null] + reporting_date date [not null, note: 'ADD: 01.04 / 01.07 / 01.10 / 01.01 след. года — для ЕХ и сравнений'] + created_at timestamptz + Indexes { (company_group_id, year, quarter) [unique] } + Note: 'Логическая консолидация ГК за период; версии различаются периметром (consolidation_member). Прошлые периоды — по company_group_id. v5.18: файлы, загруженные в консолидацию, — stored_file.consolidation_id (основа рабочего стола; полный состав — context/05-domain-rules.md §9.5 п. 11)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 (сценарий, подход, чекбокс Гудвилл); 6.10 (статусная модель, маршрут по инициатору, № и дата решения УОБ, «новая версия КФО» после Верифицирован); 6.4 (причины отклонений от Методики) +// Аналитика: md «Статусная модель»; md «Модель данных» v4 (consolidation_version: version_no, status varchar, initiator, approved_in_rf, verified_in_go, kfo_confirmation_enabled) +// Макет: «Версия», «Дата верификации», «Включить/Отключить подтверждение КФО», «Смена статуса отчёта», «Ответственный / Утвердил / Верифицировал» +// В модели: v4 + v5 (enum статусов, scenario, approach, goodwill_enabled, initiator_office_id, uob_*, methodology_deviations, previous_version_id) +Table consolidation_version { + id uuid [pk] + consolidation_id uuid [not null, ref: > consolidation.id] + version_no int [not null] + status consolidation_status [not null, note: 'CHG: enum вместо varchar'] + scenario consolidation_scenario [not null, note: 'ADD ТЗ 5.1'] + approach consolidation_approach [not null, default: 'BASIC', note: 'ADD ТЗ 5.1'] + goodwill_enabled boolean [not null, default: false, note: 'ADD ТЗ 5.1/6.7: чек-бокс расчёта Гудвилла'] + initiator_id uuid [not null, ref: > app_user.id] + initiator_office_id uuid [not null, ref: > regional_office.id, note: 'ГО/РФ инициатора — определяет маршрут (ТЗ 6.10)'] + approved_in_rf_by uuid [ref: > app_user.id] + approved_in_rf_at timestamptz + verified_in_go_by uuid [ref: > app_user.id] + verified_in_go_at timestamptz + uob_decision_no varchar [note: 'ADD ТЗ 6.10: № решения УОБ'] + uob_decision_date date [note: 'ADD ТЗ 6.10'] + methodology_deviations text [note: 'ADD ТЗ 6.4: причины отклонений от Методики'] + elimination_rules_version int [not null, note: 'v5.11; v5.23 not null: версия elimination_rule (version_no), действующая на дату создания версии консолидации (OQ-174); фиксируется при создании, не меняется'] + is_in_work boolean [not null, default: true] + previous_version_id uuid [ref: > consolidation_version.id, note: '«новая версия КФО» после VERIFIED'] + cancelled_reason text + created_at timestamptz + created_by uuid [ref: > app_user.id] + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { (consolidation_id, version_no) [unique] } + Note: 'Версия консолидации. После VERIFIED — read-only; изменения только через новую версию' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 (переходы, возврат на доработку); 8.2 «изменение статусов; согласование и утверждение КФО» +// Аналитика: md «Статусная модель» (переходы) +// Макет: «Смена статуса отчёта», «Комментарий (обязательно)», «Отклонено: отчёт возвращён на доработку» +// В модели: v5 новая +Table consolidation_status_history { + id uuid [pk] + version_id uuid [not null, ref: > consolidation_version.id] + from_status consolidation_status + to_status consolidation_status [not null] + changed_by uuid [not null, ref: > app_user.id] + changed_at timestamptz [not null] + comment text [note: 'обязателен при возврате на доработку'] + Note: 'ADD ТЗ 6.10/8.2: маршрут согласования' +} + +// ADD v5.1 (макет, ANL-036): «взять в работу» / перехват прав редактирования +// ИСТОЧНИК: +// ТЗ: нет в ТЗ (ANL-036) +// Аналитика: нет +// Макет: «Взятие КФО в работу (перехват прав редактирования)», «Причина: Отпуск», «В данный момент консолидация редактируется пользователем …» +// В модели: v5.1 новая (макет) +// Зачем: блокировка версии; подтвердить с заказчиком +// v5.16: без истечения по времени (OQ-119): активна, пока не снята владельцем/администратором или не перехвачена по причине +Table consolidation_lock { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + locked_by uuid [not null, ref: > app_user.id] + locked_at timestamptz [not null] + released_at timestamptz [note: 'NULL = активная блокировка'] + taken_over_from uuid [ref: > app_user.id, note: 'при перехвате — предыдущий владелец'] + takeover_reason text [note: 'обязателен при перехвате (макет: «Причина: Отпуск»)'] + Indexes { consolidation_version_id [note: 'partial UNIQUE WHERE released_at IS NULL'] } + Note: 'Ответственный за версию сейчас = активная блокировка' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 «КА в рамках консолидации редактирует состав ГСК»; 6.10 «выбор состава ГСК»; ЕХ 6.3 «Периметр на дату» +// Аналитика: md «Сводная ОСВ» п.2 («зафиксированный периметр; изменение related_group_member не меняет версию»); md «Модель данных» v4 +// Макет: «Состав ЮЛ», «Минимум для консолидации: 2», «Редактировать состав» +// В модели: v4 + v5 (is_head_entity для Гудвилла) +Table consolidation_member { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + is_head_entity boolean [not null, default: false, note: 'ADD: головная организация (для Гудвилла)'] + Indexes { (consolidation_version_id, legal_entity_id) [unique] } + Note: 'Зафиксированный периметр версии. Изменение related_group_member не меняет существующие версии' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Документ ФО: ссылка на Консолидацию и ЮЛ»; 6.4/6.5 «использование данных предыдущих периодов»; ЕХ 7 «повторное использование первички без дублей» +// Аналитика: md «Сводная ОСВ» п.3 (файлы выбираются по ЮЛ+год+квартал — без явной связи) +// Макет: «Первичные документы консолидации» +// В модели: v5 новая; v5.13 — document_id вместо stored_file_id +// Зачем: явная привязка документа к версии (переиспользование, монозаёмщики из ЕХ) +// v5.16: точки привязки/отвязки — подтверждение документа, добавление ЮЛ в периметр; замена/удаление файла, исключение ЮЛ (Срез 4, D-5) +Table consolidation_document { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + document_id uuid [not null, ref: > document.id, note: 'v5.13: логический документ (ОСВ по счёту / ББ / ОФР), не файл'] + Indexes { (consolidation_version_id, document_id) [unique] } + Note: 'ADD Прил. 2 ТЗ «Документ ФО → Консолидация»: явная привязка документов к версии (переиспользование в нескольких версиях/периодах, монозаёмщики из ЕХ). Чек-лист = required_document_rule × периметр − consolidation_document' +} + + +// ---- Назначения ВГО (AURORA-1751; context/05-domain-rules.md §5) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 3/4 (пары счетов, правила элиминации) +// Аналитика: DOC-272613 «Корректировка данных агрегатом» («Ручное добавление и перенос строк», §5); md «Автоматическое сопоставление ВГО» +// Макет: «Таблица корректировок» — перенос строки в правило / «Не учитывать в КФО» +// В модели: v5.24 новая +// Зачем: ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения по правилам (§5): +// привязка к правилу, снятие привязки, исключение/включение нераспределённой строки в КФО. Само +// разнесение по-прежнему чистая функция и не хранится (DECISION-5) — здесь только перекрывающее её решение. +Table vgo_assignment { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null] + osv_entry_id uuid [not null, note: 'строка сводной ОСВ (исходная), FK → osv_entry.id, ON DELETE CASCADE — см. Ref ниже'] + rule_id uuid [ref: > elimination_rule.id, note: 'NULL при action = UNASSIGNED'] + action varchar [not null, note: 'ASSIGN | UNASSIGNED (CHECK в БД, в DBML не представлено); ASSIGN — только с rule_id, UNASSIGNED — без'] + is_exclude boolean [not null, default: true, note: '«Не учитывать в КФО»; CHECK в БД: true допустимо только при rule_id = NULL'] + created_by uuid [ref: > app_user.id] + created_at timestamptz [not null, default: `now()`] + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { + (consolidation_version_id, statement_type, osv_entry_id) [unique] + osv_entry_id + } + Note: 'Только ручные решения аналитика поверх автоматического разнесения строк ВГО; одна действующая запись на строку ОСВ в версии и типе отчётности — повторное решение обновляет её. Поле side (черновик контракта dev/api-contract-corrections.md §5, строка с одновременно дебетовым и кредитовым сальдо) не добавлено — решение одно на обе позиции строки' +} +Ref: vgo_assignment.osv_entry_id > osv_entry.id [delete: cascade] + + +// ---- Корректировки ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.8 (форма корректировок: колонки ДБ/КР/Комментарий, >5%, импорт/экспорт, отдельная форма ОФР); 6.4 (доп. корректировки ОФР); Прил. 2 «Расхождение/корректировка»; Прил. 9 +// Аналитика: md «Корректировка данных» (correction: parent_correction_id, debit/credit_line_code, adjustment); md «Модель данных» v4 +// Макет: «Таблица корректировки»: Актив/Пассив/Контроль/Корректировка, «Строка баланса Дт/Кт», «Комментарий» +// В модели: v4 + v5 (statement_type, pair_match_id, reason_code, comment NOT NULL, status, version_no) +Table correction { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null, note: 'ADD: ББ или ОФР (ТЗ 6.8 — отдельные формы)'] + rule_id uuid [ref: > elimination_rule.id, note: 'v5.10: пара ВГО по естественному ключу (правило + компании); NULL = ручная без пары'] + parent_correction_id uuid [ref: > correction.id, note: 'дочерняя строка распределения по нескольким статьям; глубина 1'] + legal_entity_id uuid [ref: > legal_entity.id, note: 'компания 1 (чья первичка корректируется)'] + counterparty_id uuid [ref: > legal_entity.id, note: 'компания 2'] + debit_line_code varchar + debit_account_id uuid [ref: > chart_of_account.id] + debit_adjustment numeric(18,2) + credit_line_code varchar + credit_account_id uuid [ref: > chart_of_account.id] + credit_adjustment numeric(18,2) + deviation_amount numeric(18,2) + deviation_percent numeric(9,4) + reason_code varchar [note: 'ADD ТЗ 6.4: unrealized_revenue | excise | agency_commission | intra_lease_depreciation | intra_loan_interest | equity_income | other'] + comment text [not null, note: 'ТЗ 6.8: комментарий обязателен'] + status correction_status [not null, default: 'DRAFT'] + version_no int [not null, default: 1, note: 'сноска 22 ТЗ: история версий корректировки — audit_event(CORRECTION_EDIT, before/after); инкремент при каждом изменении'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { + consolidation_version_id + (consolidation_version_id, rule_id, legal_entity_id, counterparty_id) + } + Note: 'Overlay поверх первички: итог = свод первички − Σ применённых корректировок. Расхождение пары (deviation_*) — справочно, пересчитывается' +} + + +// ---- Гудвилл (ТЗ 6.7, Прил. 3 п.2, Прил. 8) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.7 (Гудвилл: ФВ ГО − доля в капитале ДО, комментарий обязателен, отдельная строка ББ); Прил. 3 п.2 (58.01 против 80/83, стр. 1310/1350/1370); Прил. 8 +// Аналитика: md «Гудвилл» (шаги 1–10: 58.01, стр. 1310, доля владения, include_in_kfo); md «Модель данных» v4 (goodwill) +// Макет: «Таблица гудвилла», «Доля владения», «Учет в отчете», «Изменение долей владения (гудвилл)» +// В модели: v4 + v5 (additional_capital_amount, comment) +Table goodwill { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + head_legal_entity_id uuid [not null, ref: > legal_entity.id] + subsidiary_legal_entity_id uuid [not null, ref: > legal_entity.id] + ownership_share numeric(7,4) [note: 'ручной ввод, %'] + financial_investment_amount numeric(18,2) [note: 'ΣДБ 58.01 ГО по ДО'] + charter_capital_amount numeric(18,2) [note: 'стр. 1310 ББ ДО на отчётную дату'] + additional_capital_amount numeric(18,2) [note: 'ADD ТЗ Прил. 3: стр. 1350 ББ ДО'] + participation_amount numeric(18,2) [note: '(1310 + 1350) * share / 100 — DECISION: уточнить у методологов, входит ли 1350 в базу'] + goodwill_amount numeric(18,2) + include_in_kfo boolean [not null, default: false] + comment text [note: 'ТЗ 6.7: обязателен при include_in_kfo'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { (consolidation_version_id, head_legal_entity_id, subsidiary_legal_entity_id) [unique] } +} + +// ---- Результат КФО (ТЗ 6.4, Прил. 10) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 (итоговая форма КФО: подход, Гудвилл, расхождения, корректировки, верификация); 6.8 (контрольные пороги 10% ЧА / 1% ВБ, 10% прибыли / 1% выручки); Прил. 10 +// Аналитика: нет (пробел — ANL-008/009) +// Макет: «КФО», «Сводка расхождений КФО», «Есть несохранённые изменения в КФО» +// В модели: v5 новая +// Зачем: шапка результата + пороги +Table kfo_result { + id uuid [pk] + consolidation_version_id uuid [not null, unique, ref: > consolidation_version.id] + computed_at timestamptz [not null] + is_frozen boolean [not null, default: false, note: 'true после VERIFIED (ставит переход статуса, AURORA-2002); дальше не пересчитывается'] + net_assets numeric(18,2) [note: 'ЧА ГСК = строка 1300 КФО банка (OQ-187)'] + balance_total numeric(18,2) [note: 'ВБ ГСК = строка 1600 КФО банка'] + revenue numeric(18,2) [note: 'строка 2110 КФО банка'] + sales_profit numeric(18,2) [note: 'строка 2200 КФО банка'] + total_deviation_bb numeric(18,2) [note: 'Σ расхождений по строкам бланка ББ без итогов (§8 «КФО» п. 9)'] + total_deviation_ofr numeric(18,2) + threshold_bb_exceeded boolean [note: 'ТЗ 6.8: > 10% ЧА (или > 1% ВБ, если ЧА < 10% ВБ, в т. ч. ЧА ≤ 0); равно — норма (§8 «КФО» п. 10)'] + threshold_ofr_exceeded boolean [note: '> 10% прибыли от продаж (или > 1% выручки при рентабельности < 10% или 2200 < 0; 2110 = 0 — предел 0)'] + threshold_justification text [note: 'обоснование принятия риска при превышении'] + goodwill_total numeric(18,2) [note: 'v5.26: Σ G применимых пар гудвилла, тыс. руб. = bank_amount строки GOODWILL (§8 «КФО» п. 8)'] + has_deviations boolean + warnings jsonb [note: 'v5.26: предупреждения расчёта [{code, message, legal_entity_id?, statement_type?, count?}], коды — §8 «КФО» п. 12'] + corrections_count_bb int [note: 'v5.26: число агрегатов ББ с применённой корректировкой (§8 «КФО» п. 11)'] + corrections_count_ofr int + corrections_amount_bb numeric(18,2) [note: 'v5.26: Σ исключений этих корректировок обеих сторон = Σ eliminated_manual строк бланка ББ без итогов, тыс. руб.'] + corrections_amount_ofr numeric(18,2) + Note: 'Шапка результата КФО + контрольные пороги. Все суммы — тыс. руб. (§8 «КФО»); одна запись на версию, пересчитывается при чтении, пока не is_frozen' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 «шаг 1 — сложение по кодам строк; шаг 2 — элиминация; неэлиминируемая часть остаётся»; Прил. 2 «Результат КФО: статья, значение»; Прил. 10; ФСБУ 4/2023 +// Аналитика: DOC-274636 «Расчёт КФО» (черновик), DOC-274230, DOC-259025, DOC-272613; выжимка — context/05-domain-rules.md §8 «КФО» +// Макет: колонки «в т.ч. взаимные обязательства», «Очищенные показатели», «Расхождения», «источник: Баланс/ОСВ», «в т.ч. Гудвилл» +// В модели: v5 новая + v5.1 (vgo_amount, source_kind, deviation_amount, comment) +Table kfo_result_line { + id uuid [pk] + kfo_result_id uuid [not null, ref: > kfo_result.id] + statement_type statement_type [not null] + line_code varchar [not null, note: 'код строки бланка редакции на отчётную дату (66н / ФСБУ 4/2023); GOODWILL — служебная строка раздела I после 1170, без разреза по компаниям (§8 «КФО» п. 8, OQ-185)'] + line_name varchar + sum_amount numeric(18,2) [note: 'Σ client_amount по компаниям, тыс. руб.'] + eliminated_auto numeric(18,2) [note: 'v5.26: часть vgo от агрегатов без корректировки и гудвилл-пар (автосумма по elimination_mode)'] + eliminated_manual numeric(18,2) [note: 'v5.26: часть vgo от агрегатов с применённой корректировкой; корректировка заменяет автосумму агрегата, не складывается с ней'] + goodwill_adjust numeric(18,2) [note: 'v5.26: Σ G — у строки GOODWILL и у итогов, куда она входит (1100, 1600)'] + consolidated_amount numeric(18,2) [not null, note: '«КФО, подготовленная банком» = sum_amount − vgo_amount + goodwill_adjust'] + vgo_amount numeric(18,2) [note: '«в т.ч. взаимные обязательства/обороты» = Σ vgo по компаниям = eliminated_auto + eliminated_manual; со знаком строки (у расходов < 0)'] + source_kind varchar [note: 'v5.1 макет: balance | osv; v5.26: всегда balance — значение из формы (OQ-182)'] + deviation_amount numeric(18,2) [note: '«Расхождения» по статье: Σ |amount_1 − amount_2| агрегатов min на статье большей стороны (§8 «КФО» п. 9); у итогов — null'] + comment text [note: 'v5.1 макет: пояснение к строке КФО; ≤ 2000, переносится при пересчёте по (statement_type, line_code)'] + ordinal int + Indexes { (kfo_result_id, statement_type, line_code) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.6 «выгрузка расшифровки статей отчётности в разрезе контрагентов»; ТЗ 6.9 (настраиваемые поля экспорта) +// Аналитика: нет +// Макет: «Доля в показателе по группе, %», расшифровка по компаниям +// В модели: v5 новая + v5.1 (share_percent) +Table kfo_result_line_company { + kfo_result_line_id uuid [not null, ref: > kfo_result_line.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + client_amount numeric(18,2) [note: 'v5.26: «По данным клиента», тыс. руб.; строки расходов приведены к −|x| (§8 «КФО» п. 2)'] + vgo_amount numeric(18,2) [note: 'v5.26: «В т. ч. взаимные», тыс. руб. до целых, со знаком строки (§8 «КФО» п. 6)'] + amount numeric(18,2) [note: '«Очищенные показатели» = client_amount − vgo_amount'] + share_percent numeric(7,4) [note: 'v5.1 макет: «Доля в показателе по группе, %» = amount / kfo_result_line.consolidated_amount × 100, ROUND_HALF_UP; null при нулевом итоге или |доля| ≥ 1000 % (иначе переполнение numeric(7,4))'] + Indexes { (kfo_result_line_id, legal_entity_id) [pk] } + Note: 'Разрез по компаниям для расшифровки статьи (ЕХ 6.6 «расшифровка в разрезе контрагентов»)' +} + +// ---- Сценарий «Подтверждение КФО клиента» (ТЗ 5.1, 6.10) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 «подтверждение консолидированной отчётности, предоставленной Клиентом»; 6.10 «ручной ввод данных КФО клиента; сравнение и анализ расхождений»; Методика сноска 14 (процент к наименьшему) +// Аналитика: md «Статусная модель» (kfo_confirmation_enabled — только флаг; ANL-006) +// Макет: «Подтверждение КФО клиента»: Версия клиента / Версия банка, ВГО клиента / ВГО Банка, Расхождение, Причина +// В модели: v5 новая + v5.1 (client_vgo/bank_vgo/deviation_reason) +Table client_kfo_line { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null] + line_code varchar [not null] + client_amount numeric(18,2) [note: 'ручной ввод из КФО клиента («Консолидированная отчетность от клиента»)'] + client_vgo_amount numeric(18,2) [note: 'v5.1 макет: «ВГО клиента» — ручной ввод'] + bank_amount numeric(18,2) [note: 'из kfo_result_line на момент сравнения'] + bank_vgo_amount numeric(18,2) [note: 'v5.1 макет: «ВГО Банка» из kfo_result_line.vgo_amount'] + deviation_amount numeric(18,2) + deviation_percent numeric(9,4) [note: 'относительно наименьшего (Методика сноска 14)'] + deviation_reason text [note: 'v5.1 макет: «Причина расхождения»'] + comment text + entered_by uuid [ref: > app_user.id] + entered_at timestamptz + Indexes { (consolidation_version_id, statement_type, line_code) [unique] } +} + + + +// ============ КОММЕНТАРИИ, АУДИТ, ЭКСПОРТ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 «возможность добавления комментария к отчётности» +// Аналитика: md «Загрузка документов» («Комментарий к документу») +// Макет: «Новый комментарий», «Комментарий к документу» +// В модели: v5 новая +// Зачем: полиморфные комментарии к объектам; сейчас используется привязка к документу +Table comment { + id uuid [pk] + object_type varchar [not null, note: 'document | consolidation_version | …'] + object_id uuid [not null] + text text [not null] + author_id uuid [not null, ref: > app_user.id] + created_at timestamptz [not null] + Indexes { (object_type, object_id) } + Note: 'ТЗ 6.3; AURORA-1446: комментарии к документу. Правка и удаление комментариев не предусмотрены; аудит не ведётся' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.2 (журнал изменений, логирование всех действий, перечень событий: вход/выход, создание консолидации, ГСК/ГК, загрузка, распознанные данные, корректировки, статусы, утверждение, выгрузка; хранение ≥ срока данных); Прил. 2 «Запись журнала изменений» +// Аналитика: нет (пробел — ANL-019); md «Ролевая модель» ссылается на «журнал аудита» +// Макет: «Журнал действий»: разделы, Объект/Событие/Что изменилось/Результат/Идентификатор, IP/браузер, «неверный пароль» +// В модели: v5 новая + v5.1 (section, result, changed_field, user_agent) +Table audit_event { + id uuid [pk] + occurred_at timestamptz [not null] + user_id uuid [ref: > app_user.id, note: 'NULL для системных событий'] + action audit_action [not null] + section audit_section [not null, note: 'v5.1 макет: раздел журнала'] + object_type varchar [not null] + object_id uuid + consolidation_version_id uuid [ref: > consolidation_version.id, note: 'для выборки «история изменений КФО»'] + description text + before_value jsonb + after_value jsonb + comment text + result varchar [note: 'v5.1 макет: Успешно / Отклонено / Ошибка'] + changed_field varchar [note: 'v5.1 макет: фильтр «Изменяемое поле»'] + ip_address varchar + user_agent varchar [note: 'v5.1 макет: браузер в событиях сеанса'] + Indexes { + (consolidation_version_id, occurred_at) + (object_type, object_id) + (user_id, occurred_at) + } + Note: 'ТЗ 8.2: журнал изменений/аудит. Append-only. Хранение ≥ срока хранения консолидации. Также покрывает: историю правок распознанных данных, факт/состав выгрузок (ЕХ 6.6), импорт справочников, изменения матрицы прав' +} + + + +// ============ ЕХФД (ТЗ BIQ-34649) ============ +// DECISION-2: срез = замороженный указатель на верифицированную версию + список её артефактов. + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.2 «запись срезами: одна ГСК на одну отчётную дату, после Верифицирован»; 6.4 «версии среза Актуальная/Архив, неизменяемость»; 5.3 «архивирование в data lake» +// Аналитика: нет (пробел — ANL-032) +// Макет: «Единое хранилище», «Сформированная отчетность», «Верифицирована» +// В модели: v5 новая (DECISION-2) +// Зачем: замороженный указатель на верифицированную версию +Table ex_snapshot { + id uuid [pk] + related_group_id uuid [not null, ref: > related_group.id] + company_group_id uuid [not null, ref: > company_group.id, note: 'денормализация на момент верификации'] + reporting_date date [not null] + year smallint [not null] + quarter period_quarter [not null] + version_no int [not null] + status ex_snapshot_status [not null, note: 'ACTUAL — последняя верифицированная; предыдущие → ARCHIVE'] + source_version_id uuid [not null, unique, note: 'v5.12: id consolidation_version-источника БЕЗ FK — источник удаляется по сроку, срез нет'] + source_version_no int [not null] + superseded_by_snapshot_id uuid [ref: > ex_snapshot.id, note: 'v5.12: для ARCHIVE — ссылка на актуальный срез, который его заместил'] + verified_at timestamptz [not null] + written_at timestamptz [not null] + written_by uuid [ref: > app_user.id, note: 'инициатор записи'] + archived_at timestamptz + data_lake_ref varchar [note: 'ссылка на архив в data lake'] + Indexes { + (related_group_id, year, quarter, version_no) [unique] + (related_group_id, year, quarter, status) + } + Note: 'Срез данных ЕХ: одна ГСК на одну отчётную дату (ЕХ 6.2). Неизменяем и самодостаточен (не зависит от таблиц консолидации). Состав — ex_snapshot_item + ex_snapshot_member' +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.3 «Периметр на дату: состав ЮЛ/ИП группы на отчётную дату» +// Аналитика: нет +// Макет: дерево «ГК → ГСК → Организации» с ИНН +// В модели: v5 новая +// Зачем: денормализация периметра на дату среза +Table ex_snapshot_member { + ex_snapshot_id uuid [not null, ref: > ex_snapshot.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + inn varchar [not null, note: 'денормализация на дату среза'] + name varchar [not null] + Indexes { (ex_snapshot_id, legal_entity_id) [pk] } + Note: 'Периметр на дату (ЕХ 6.3)' +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 5.2 «Состав хранимых данных» (КФО, первичные, сводная ОСВ, шахматка, гудвилл, корректировки, иные отчёты); 6.7.3/6.7.4 «Сформированная отчётность / Первичные документы», «Просмотр / Выгрузка» +// Аналитика: нет +// Макет: область документов ЕХ, «Скачать», «Связать» документ с организацией +// В модели: v5 новая; v5.13 — document_id вместо stored_file_id +// Зачем: состав среза со ссылками на объекты +Table ex_snapshot_item { + id uuid [pk] + ex_snapshot_id uuid [not null, ref: > ex_snapshot.id] + item_type varchar [not null, note: 'primary_document | summary_osv | vgo_pairs | chess | kfo_balance | kfo_income | goodwill | corrections | other_report'] + legal_entity_id uuid [ref: > legal_entity.id, note: 'для первички — организация'] + document_id uuid [ref: > document.id, note: 'v5.13: только для primary_document — исходники хранятся весь срок, FK допустим; «Скачать» — через document.stored_file → s3_object'] + payload jsonb [note: 'v5.12 DECISION-5: замороженные данные для всех остальных типов; самодостаточно, без ссылок на таблицы консолидации'] + payload_schema_version varchar + Indexes { (ex_snapshot_id, item_type) } + Note: 'Состав среза: primary_document → document; всё остальное → payload. «Просмотр» в ЛК ЕХ рендерит payload по схеме; «Выгрузка» собирает xlsx из payload на лету' +} + +// ============ ПРОЧЕЕ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 (базовый / расширенный пакет ОСВ); 6.2 «набор обязательных ОСВ»; сноска 12 «процесс останавливается, если отсутствует документ» +// Аналитика: md «Загрузка документов» (чек-лист: «ОСВ по обязательным счетам», «прочие документы») +// Макет: «Чек-лист загруженных документов», «Условные обозначения статусов» +// В модели: v5 новая +// Зачем: конфигурация чек-листа по подходу; с v5.13 сверяется с document (тип + счёт + период), а не с файлом +Table required_document_rule { + id uuid [pk] + approach consolidation_approach [not null] + document_type document_type [not null] + account_id uuid [ref: > chart_of_account.id, note: 'для OSV'] + is_mandatory boolean [not null, note: 'обязателен → блокирует переход к консолидации (сноска 12 ТЗ)'] + Indexes { (approach, document_type, account_id) [unique] } + Note: 'ADD ТЗ 5.1/6.2: состав обязательных документов по подходу — основа чек-листа. Выполнено = есть document CONFIRMED с таким типом/счётом за период у каждого ЮЛ периметра' +} diff --git a/model/diagrams/part08_corrections_kfo.png b/model/diagrams/part08_corrections_kfo.png index 0ebb094..dfabe9d 100644 Binary files a/model/diagrams/part08_corrections_kfo.png and b/model/diagrams/part08_corrections_kfo.png differ diff --git a/model/diagrams/part08_corrections_kfo.svg b/model/diagrams/part08_corrections_kfo.svg index 095329d..44d57f3 100644 --- a/model/diagrams/part08_corrections_kfo.svg +++ b/model/diagrams/part08_corrections_kfo.svg @@ -4,57 +4,57 @@ - - + + dbml statement_type - - - -       statement_type        - - -    balance     - - -    income     - + + + +       statement_type        + + +    balance     + + +    income     + correction_status - - - -       correction_status        - - -    DRAFT     - - -    APPLIED     - - -    REVOKED     - + + + +       correction_status        + + +    DRAFT     + + +    APPLIED     + + +    REVOKED     + app_user - - - -       app_user        - - -id -     -uuid - + + + +       app_user        + + +id +     +uuid + @@ -62,16 +62,16 @@ chart_of_account - - - -       chart_of_account        - - -id -     -uuid - + + + +       chart_of_account        + + +id +     +uuid + @@ -79,16 +79,16 @@ consolidation_version - - - -       consolidation_version        - - -id -     -uuid - + + + +       consolidation_version        + + +id +     +uuid + @@ -96,16 +96,16 @@ elimination_rule - - - -       elimination_rule        - - -id -     -uuid - + + + +       elimination_rule        + + +id +     +uuid + @@ -113,16 +113,16 @@ legal_entity - - - -       legal_entity        - - -id -     -uuid - + + + +       legal_entity        + + +id +     +uuid + @@ -130,521 +130,541 @@ correction - - - -       correction        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -rule_id     -uuid - - -parent_correction_id     -uuid - - -legal_entity_id     -uuid - - -counterparty_id     -uuid - - -debit_line_code     -varchar - - -debit_account_id     -uuid - - -debit_adjustment     -numeric(18,2) - - -credit_line_code     -varchar - - -credit_account_id     -uuid - - -credit_adjustment     -numeric(18,2) - - -deviation_amount     -numeric(18,2) - - -deviation_percent     -numeric(9,4) - - -reason_code     -varchar - - -comment     -text - -(!) - - -status     -correction_status - -(!) - - -version_no     -int - -(!) - - -created_by     -uuid - - -created_at     -timestamptz - - -updated_by     -uuid - - -updated_at     -timestamptz - + + + +       correction        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +rule_id     +uuid + + +parent_correction_id     +uuid + + +legal_entity_id     +uuid + + +counterparty_id     +uuid + + +debit_line_code     +varchar + + +debit_account_id     +uuid + + +debit_adjustment     +numeric(18,2) + + +credit_line_code     +varchar + + +credit_account_id     +uuid + + +credit_adjustment     +numeric(18,2) + + +deviation_amount     +numeric(18,2) + + +deviation_percent     +numeric(9,4) + + +reason_code     +varchar + + +comment     +text + +(!) + + +status     +correction_status + +(!) + + +version_no     +int + +(!) + + +created_by     +uuid + + +created_at     +timestamptz + + +updated_by     +uuid + + +updated_at     +timestamptz + correction:e->statement_type:w - + correction:e->correction_status:w - + correction:e->app_user:w - - -1 -* + + +1 +* correction:e->app_user:w - - -1 -* + + +1 +* correction:e->chart_of_account:w - - -1 -* + + +1 +* correction:e->chart_of_account:w - - -1 -* + + +1 +* correction:e->consolidation_version:w - - -1 -* + + +1 +* correction:e->elimination_rule:w - - -1 -* + + +1 +* correction:e->legal_entity:w - - -1 -* + + +1 +* correction:e->legal_entity:w - - -1 -* + + +1 +* correction:e->correction:w - - -1 -* + + +1 +* vgo_assignment - - - -       vgo_assignment        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -osv_entry_id     -uuid - -(!) - - -rule_id     -uuid - - -action     -varchar - -(!) - - -is_exclude     -boolean - -(!) - - -created_by     -uuid - - -created_at     -timestamptz - -(!) - - -updated_by     -uuid - - -updated_at     -timestamptz - + + + +       vgo_assignment        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +osv_entry_id     +uuid + +(!) + + +rule_id     +uuid + + +action     +varchar + +(!) + + +is_exclude     +boolean + +(!) + + +created_by     +uuid + + +created_at     +timestamptz + +(!) + + +updated_by     +uuid + + +updated_at     +timestamptz + vgo_assignment:e->statement_type:w - + vgo_assignment:e->app_user:w - - -1 -* + + +1 +* vgo_assignment:e->app_user:w - - -1 -* + + +1 +* vgo_assignment:e->consolidation_version:w - - -1 -* + + +1 +* vgo_assignment:e->elimination_rule:w - - -1 -* + + +1 +* goodwill - - - -       goodwill        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -head_legal_entity_id     -uuid - -(!) - - -subsidiary_legal_entity_id     -uuid - -(!) - - -ownership_share     -numeric(7,4) - - -financial_investment_amount     -numeric(18,2) - - -charter_capital_amount     -numeric(18,2) - - -additional_capital_amount     -numeric(18,2) - - -participation_amount     -numeric(18,2) - - -goodwill_amount     -numeric(18,2) - - -include_in_kfo     -boolean - -(!) - - -comment     -text - - -created_by     -uuid - - -created_at     -timestamptz - - -updated_by     -uuid - - -updated_at     -timestamptz - + + + +       goodwill        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +head_legal_entity_id     +uuid + +(!) + + +subsidiary_legal_entity_id     +uuid + +(!) + + +ownership_share     +numeric(7,4) + + +financial_investment_amount     +numeric(18,2) + + +charter_capital_amount     +numeric(18,2) + + +additional_capital_amount     +numeric(18,2) + + +participation_amount     +numeric(18,2) + + +goodwill_amount     +numeric(18,2) + + +include_in_kfo     +boolean + +(!) + + +comment     +text + + +created_by     +uuid + + +created_at     +timestamptz + + +updated_by     +uuid + + +updated_at     +timestamptz + goodwill:e->app_user:w - - -1 -* + + +1 +* goodwill:e->app_user:w - - -1 -* + + +1 +* goodwill:e->consolidation_version:w - - -1 -* + + +1 +* goodwill:e->legal_entity:w - - -1 -* + + +1 +* goodwill:e->legal_entity:w - - -1 -* + + +1 +* kfo_result - - - - -       kfo_result        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -computed_at     -timestamptz - -(!) - - -is_frozen     -boolean - -(!) - - -net_assets     -numeric(18,2) - - -balance_total     -numeric(18,2) - - -revenue     -numeric(18,2) - - -sales_profit     -numeric(18,2) - - -total_deviation_bb     -numeric(18,2) - - -total_deviation_ofr     -numeric(18,2) - - -threshold_bb_exceeded     -boolean - - -threshold_ofr_exceeded     -boolean - - -threshold_justification     -text - - -goodwill_total     -numeric(18,2) - - -has_deviations     -boolean - + + + + +       kfo_result        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +computed_at     +timestamptz + +(!) + + +is_frozen     +boolean + +(!) + + +net_assets     +numeric(18,2) + + +balance_total     +numeric(18,2) + + +revenue     +numeric(18,2) + + +sales_profit     +numeric(18,2) + + +total_deviation_bb     +numeric(18,2) + + +total_deviation_ofr     +numeric(18,2) + + +threshold_bb_exceeded     +boolean + + +threshold_ofr_exceeded     +boolean + + +threshold_justification     +text + + +goodwill_total     +numeric(18,2) + + +has_deviations     +boolean + + +warnings     +jsonb + + +corrections_count_bb     +int + + +corrections_count_ofr     +int + + +corrections_amount_bb     +numeric(18,2) + + +corrections_amount_ofr     +numeric(18,2) + @@ -652,134 +672,142 @@ kfo_result:e->consolidation_version:w - - -1 -* + + +1 +* kfo_result_line - - - -       kfo_result_line        - - -id -     -uuid - - -kfo_result_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -line_code     -varchar - -(!) - - -line_name     -varchar - - -sum_amount     -numeric(18,2) - - -eliminated_auto     -numeric(18,2) - - -eliminated_manual     -numeric(18,2) - - -goodwill_adjust     -numeric(18,2) - - -consolidated_amount     -numeric(18,2) - -(!) - - -vgo_amount     -numeric(18,2) - - -source_kind     -varchar - - -deviation_amount     -numeric(18,2) - - -comment     -text - - -ordinal     -int - + + + +       kfo_result_line        + + +id +     +uuid + + +kfo_result_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +line_code     +varchar + +(!) + + +line_name     +varchar + + +sum_amount     +numeric(18,2) + + +eliminated_auto     +numeric(18,2) + + +eliminated_manual     +numeric(18,2) + + +goodwill_adjust     +numeric(18,2) + + +consolidated_amount     +numeric(18,2) + +(!) + + +vgo_amount     +numeric(18,2) + + +source_kind     +varchar + + +deviation_amount     +numeric(18,2) + + +comment     +text + + +ordinal     +int + kfo_result_line:e->statement_type:w - + kfo_result_line:e->kfo_result:w - - -1 -* + + +1 +* kfo_result_line_company - - - -       kfo_result_line_company        - - -kfo_result_line_id -     -uuid - -(!) - - -legal_entity_id -     -uuid - -(!) - - -amount     -numeric(18,2) - - -share_percent     -numeric(7,4) - + + + +       kfo_result_line_company        + + +kfo_result_line_id +     +uuid + +(!) + + +legal_entity_id +     +uuid + +(!) + + +client_amount     +numeric(18,2) + + +vgo_amount     +numeric(18,2) + + +amount     +numeric(18,2) + + +share_percent     +numeric(7,4) + @@ -787,114 +815,114 @@ kfo_result_line_company:e->legal_entity:w - - -1 -* + + +1 +* kfo_result_line_company:e->kfo_result_line:w - - -1 -* + + +1 +* client_kfo_line - - - -       client_kfo_line        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -line_code     -varchar - -(!) - - -client_amount     -numeric(18,2) - - -client_vgo_amount     -numeric(18,2) - - -bank_amount     -numeric(18,2) - - -bank_vgo_amount     -numeric(18,2) - - -deviation_amount     -numeric(18,2) - - -deviation_percent     -numeric(9,4) - - -deviation_reason     -text - - -comment     -text - - -entered_by     -uuid - - -entered_at     -timestamptz - + + + +       client_kfo_line        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +line_code     +varchar + +(!) + + +client_amount     +numeric(18,2) + + +client_vgo_amount     +numeric(18,2) + + +bank_amount     +numeric(18,2) + + +bank_vgo_amount     +numeric(18,2) + + +deviation_amount     +numeric(18,2) + + +deviation_percent     +numeric(9,4) + + +deviation_reason     +text + + +comment     +text + + +entered_by     +uuid + + +entered_at     +timestamptz + client_kfo_line:e->statement_type:w - + client_kfo_line:e->app_user:w - - -1 -* + + +1 +* client_kfo_line:e->consolidation_version:w - - -1 -* + + +1 +* diff --git a/model/diagrams/part09_audit_ex.png b/model/diagrams/part09_audit_ex.png index e725a5b..0e1c1f0 100644 Binary files a/model/diagrams/part09_audit_ex.png and b/model/diagrams/part09_audit_ex.png differ diff --git a/model/diagrams/part09_audit_ex.svg b/model/diagrams/part09_audit_ex.svg index 5bc3208..b5d103e 100644 --- a/model/diagrams/part09_audit_ex.svg +++ b/model/diagrams/part09_audit_ex.svg @@ -4,45 +4,45 @@ - - + + dbml period_quarter - - - -       period_quarter        - - -    Q1     - - -    Q2     - - -    Q3     - - -    Q4     - + + + +       period_quarter        + + +    Q1     + + +    Q2     + + +    Q3     + + +    Q4     + ex_snapshot_status - - - -       ex_snapshot_status        - - -    ACTUAL     - - -    ARCHIVE     - + + + +       ex_snapshot_status        + + +    ACTUAL     + + +    ARCHIVE     + @@ -83,119 +83,122 @@ audit_action - - - -       audit_action        - - -    LOGIN     - - -    LOGIN_FAILED     - - -    LOGOUT     - - -    CONSOLIDATION_CREATE     - - -    CONSOLIDATION_STATUS_CHANGE     - - -    CONSOLIDATION_APPROVE     - - -    CONSOLIDATION_VERIFY     - - -    GROUP_CHANGE     - - -    LEGAL_ENTITY_CHANGE     - - -    MEMBERSHIP_CHANGE     - - -    ASSIGNMENT_CHANGE     - - -    FILE_UPLOAD     - - -    FILE_DELETE     - - -    FILE_REPLACE     - - -    FILE_REPROCESS     - - -    DOCUMENT_CONFIRM     - - -    CONSOLIDATION_LOCK     - - -    CONSOLIDATION_LOCK_TAKEOVER     - - -    DISCLOSURE_EDIT     - - -    RECOGNIZED_DATA_EDIT     - - -    OSV_COUNTERPARTY_MAP     - - -    OSV_TYPE_OVERRIDE     - - -    CORRECTION_CREATE     - - -    CORRECTION_EDIT     - - -    CORRECTION_REVOKE     - - -    GOODWILL_EDIT     - - -    EXPORT     - - -    DIRECTORY_IMPORT     - - -    USER_ROLE_CHANGE     - - -    USER_CHANGE     - - -    ROLE_PERMISSION_CHANGE     - + + + +       audit_action        + + +    LOGIN     + + +    LOGIN_FAILED     + + +    LOGOUT     + + +    CONSOLIDATION_CREATE     + + +    CONSOLIDATION_STATUS_CHANGE     + + +    CONSOLIDATION_APPROVE     + + +    CONSOLIDATION_VERIFY     + + +    GROUP_CHANGE     + + +    LEGAL_ENTITY_CHANGE     + + +    MEMBERSHIP_CHANGE     + + +    ASSIGNMENT_CHANGE     + + +    FILE_UPLOAD     + + +    FILE_DELETE     + + +    FILE_REPLACE     + + +    FILE_REPROCESS     + + +    DOCUMENT_CONFIRM     + + +    CONSOLIDATION_LOCK     + + +    CONSOLIDATION_LOCK_TAKEOVER     + + +    DISCLOSURE_EDIT     + + +    RECOGNIZED_DATA_EDIT     + + +    OSV_COUNTERPARTY_MAP     + + +    OSV_TYPE_OVERRIDE     + + +    CORRECTION_CREATE     + + +    CORRECTION_EDIT     + + +    CORRECTION_REVOKE     + + +    GOODWILL_EDIT     + + +    KFO_EDIT     + + +    EXPORT     + + +    DIRECTORY_IMPORT     + + +    USER_ROLE_CHANGE     + + +    USER_CHANGE     + + +    ROLE_PERMISSION_CHANGE     + app_user - - - -       app_user        - - -id -     -uuid - + + + +       app_user        + + +id +     +uuid + @@ -203,16 +206,16 @@ company_group - - - -       company_group        - - -id -     -uuid - + + + +       company_group        + + +id +     +uuid + @@ -237,16 +240,16 @@ document - - - -       document        - - -id -     -uuid - + + + +       document        + + +id +     +uuid + @@ -254,16 +257,16 @@ legal_entity - - - -       legal_entity        - - -id -     -uuid - + + + +       legal_entity        + + +id +     +uuid + @@ -271,16 +274,16 @@ related_group - - - -       related_group        - - -id -     -uuid - + + + +       related_group        + + +id +     +uuid + @@ -288,46 +291,46 @@ comment - - - -       comment        - - -id -     -uuid - - -object_type     -varchar - -(!) - - -object_id     -uuid - -(!) - - -text     -text - -(!) - - -author_id     -uuid - -(!) - - -created_at     -timestamptz - -(!) - + + + +       comment        + + +id +     +uuid + + +object_type     +varchar + +(!) + + +object_id     +uuid + +(!) + + +text     +text + +(!) + + +author_id     +uuid + +(!) + + +created_at     +timestamptz + +(!) + @@ -335,304 +338,304 @@ comment:e->app_user:w - - -1 -* + + +1 +* audit_event - - - -       audit_event        - - -id -     -uuid - - -occurred_at     -timestamptz - -(!) - - -user_id     -uuid - - -action     -audit_action - -(!) - - -section     -audit_section - -(!) - - -object_type     -varchar - -(!) - - -object_id     -uuid - - -consolidation_version_id     -uuid - - -description     -text - - -before_value     -jsonb - - -after_value     -jsonb - - -comment     -text - - -result     -varchar - - -changed_field     -varchar - - -ip_address     -varchar - - -user_agent     -varchar - + + + +       audit_event        + + +id +     +uuid + + +occurred_at     +timestamptz + +(!) + + +user_id     +uuid + + +action     +audit_action + +(!) + + +section     +audit_section + +(!) + + +object_type     +varchar + +(!) + + +object_id     +uuid + + +consolidation_version_id     +uuid + + +description     +text + + +before_value     +jsonb + + +after_value     +jsonb + + +comment     +text + + +result     +varchar + + +changed_field     +varchar + + +ip_address     +varchar + + +user_agent     +varchar + audit_event:e->audit_section:w - + audit_event:e->audit_action:w - + audit_event:e->app_user:w - - -1 -* + + +1 +* audit_event:e->consolidation_version:w - + 1 -* +* ex_snapshot - - - -       ex_snapshot        - - -id -     -uuid - - -related_group_id     -uuid - -(!) - - -company_group_id     -uuid - -(!) - - -reporting_date     -date - -(!) - - -year     -smallint - -(!) - - -quarter     -period_quarter - -(!) - - -version_no     -int - -(!) - - -status     -ex_snapshot_status - -(!) - - -source_version_id     -uuid - -(!) - - -source_version_no     -int - -(!) - - -superseded_by_snapshot_id     -uuid - - -verified_at     -timestamptz - -(!) - - -written_at     -timestamptz - -(!) - - -written_by     -uuid - - -archived_at     -timestamptz - - -data_lake_ref     -varchar - + + + +       ex_snapshot        + + +id +     +uuid + + +related_group_id     +uuid + +(!) + + +company_group_id     +uuid + +(!) + + +reporting_date     +date + +(!) + + +year     +smallint + +(!) + + +quarter     +period_quarter + +(!) + + +version_no     +int + +(!) + + +status     +ex_snapshot_status + +(!) + + +source_version_id     +uuid + +(!) + + +source_version_no     +int + +(!) + + +superseded_by_snapshot_id     +uuid + + +verified_at     +timestamptz + +(!) + + +written_at     +timestamptz + +(!) + + +written_by     +uuid + + +archived_at     +timestamptz + + +data_lake_ref     +varchar + ex_snapshot:e->period_quarter:w - + ex_snapshot:e->ex_snapshot_status:w - + ex_snapshot:e->app_user:w - - -1 -* + + +1 +* ex_snapshot:e->company_group:w - - -1 -* + + +1 +* ex_snapshot:e->related_group:w - - -1 -* + + +1 +* ex_snapshot:e->ex_snapshot:w - - -1 -* + + +1 +* ex_snapshot_member - - - -       ex_snapshot_member        - - -ex_snapshot_id -     -uuid - -(!) - - -legal_entity_id -     -uuid - -(!) - - -inn     -varchar - -(!) - - -name     -varchar - -(!) - + + + +       ex_snapshot_member        + + +ex_snapshot_id +     +uuid + +(!) + + +legal_entity_id +     +uuid + +(!) + + +inn     +varchar + +(!) + + +name     +varchar + +(!) + @@ -640,62 +643,62 @@ ex_snapshot_member:e->legal_entity:w - - -1 -* + + +1 +* ex_snapshot_member:e->ex_snapshot:w - - -1 -* + + +1 +* ex_snapshot_item - - - -       ex_snapshot_item        - - -id -     -uuid - - -ex_snapshot_id     -uuid - -(!) - - -item_type     -varchar - -(!) - - -legal_entity_id     -uuid - - -document_id     -uuid - - -payload     -jsonb - - -payload_schema_version     -varchar - + + + +       ex_snapshot_item        + + +id +     +uuid + + +ex_snapshot_id     +uuid + +(!) + + +item_type     +varchar + +(!) + + +legal_entity_id     +uuid + + +document_id     +uuid + + +payload     +jsonb + + +payload_schema_version     +varchar + @@ -703,28 +706,28 @@ ex_snapshot_item:e->document:w - - -1 -* + + +1 +* ex_snapshot_item:e->legal_entity:w - - -1 -* + + +1 +* ex_snapshot_item:e->ex_snapshot:w - - -1 -* + + +1 +* diff --git a/model/entities-v5.26.md b/model/entities-v5.26.md new file mode 100644 index 0000000..acd0dd6 --- /dev/null +++ b/model/entities-v5.26.md @@ -0,0 +1,270 @@ +# АРМ КА — описание сущностей модели данных v5.26 + +Источник истины по структуре — `data-model-v5.26.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…09 svg/png; перерисовываются `make diagrams` из текущего DBML). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась. + +## Что изменилось в v5.26 (расчёт КФО, AURORA-1546/1547; RSHB/arm-ka-docs#206; миграция — в AURORA-1546) + +Номер v5.25 занят PR #203 (факты качества первички, AURORA-1759), ещё не слит; v5.26 построена от v5.24. + +| № | Изменение | Решение | +|---|---|---| +| 1 | `kfo_result_line_company` + `client_amount`, `vgo_amount` | «По данным клиента» и «в т. ч. взаимные» по компании (тыс. руб.); `amount` — «очищенные» = `client_amount − vgo_amount`. Без них колонки экрана КФО `[31-04]` из сохранённого результата не восстановить. Разреза «расхождение по компании» нет: на экране «Расхождения» — общая колонка строки | +| 2 | `kfo_result` + `warnings`, `corrections_count_bb/_ofr`, `corrections_amount_bb/_ofr` | Предупреждения расчёта и сводка корректировок для реквизитов формы хранятся с результатом: замороженная версия отдаёт то, что видели при верификации (`context/05-domain-rules.md` §8 «КФО» п. 11–12) | +| 3 | `audit_action` + `KFO_EDIT` | Сохранение ручных полей КФО (комментарии строк, обоснование порога, причины отклонений от Методики) — раздел `KFO`; подходящего действия не было | +| 4 | `elimination_rule.elimination_mode` — значение `min_or_full` | Правило ББ 2 по DOC-274230: `min` при обеих сторонах, сторона 1 при пустой стороне 2, без порога 5 %; строки ДО — каскадом 1310 → 1350 → 1370. Поле varchar — схема не меняется; seed v1 правится на месте в AURORA-1546 | +| 5 | Пояснения `kfo_result_line`, `kfo_result` | Приведены к §8 «КФО»: всё в тыс. руб.; корректировка агрегата заменяет его автосумму (`eliminated_auto` и `eliminated_manual` по одному агрегату не складываются); `GOODWILL` — служебная строка раздела I, входит в 1100/1600 | + +Остальные таблицы и enum — как в v5.24. + +## Что изменилось в v5.24 (AURORA-1751/1540/1541; RSHB/arm-ka-docs#196; миграция уже слита в arm-core main) + +| № | Изменение | Решение | +|---|---|---| +| 1 | Новая таблица `vgo_assignment` | Ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения по правилам ВГО (`context/05-domain-rules.md` §5): привязка к правилу, снятие привязки, «не учитывать в КФО» у нераспределённой строки. Само разнесение остаётся чистой функцией и не хранится (DECISION-5) — таблица только перекрывает его решением аналитика. Одна действующая запись на строку ОСВ в версии и типе отчётности | +| 2 | Поле `side` не добавлено | Черновик контракта (`dev/api-contract-corrections.md` §5) предполагал отдельную позицию для строки с одновременно дебетовым и кредитовым сальдо; в уже слитой миграции 0011 решение одно на обе позиции строки, не по стороне | + +Остальные таблицы и enum — как в v5.23. + +## Что изменилось в v5.23 (решения владельца 23.10.2026; RSHB/arm-ka-docs#164, AURORA-1536) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `consolidation_version.elimination_rules_version` — `not null` | Версия правил элиминации (`elimination_rule.version_no`), действующая на **дату создания** версии консолидации (OQ-174), фиксируется при создании и дальше не меняется. По ней считаются шахматка по правилу-пресету и пары ВГО (`context/05-domain-rules.md` §4, §5). Вариант «пусто — значит последние открытые правила» отвергнут: после публикации новой версии правил старые консолидации молча пересчитались бы по ней. Версия консолидации без действующих правил не создаётся | + +Остальные таблицы и enum — как в v5.22. Реализация — одна миграция в `arm-core`: существующим версиям проставляется номер +версии правил, действовавшей на дату `created_at` (по Москве; если такой нет — последней открытой), затем `NOT NULL`; откат +снимает только ограничение. + +## Что изменилось в v5.22 (22.10.2026; AURORA-1446) + +Из описания `comment.object_type` убраны устаревшие варианты `chess_cell` и `kfo_result_line`. +Сохранены `document | consolidation_version | …`; сейчас используется привязка к документу. Комментарий к правке пары ВГО хранится +в `correction.comment`, к строкам КФО — в `kfo_result_line.comment` и `client_kfo_line.comment`. +Структура БД не меняется, миграция не требуется. Предыдущие версии модели сохранены. + +## Что изменилось в v5.21 (решение владельца 22.10.2026; RSHB/arm-ka-docs#149, AURORA-1442) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `audit_action` + `FILE_REPROCESS` | Повторная обработка файла пользователем (`context/05-domain-rules.md` §9.5 п. 13) — отдельное событие журнала, раздел `PRIMARY_DOCUMENTS`. Файл не загружается заново: создаётся новое `recognition_job` с `attempt + 1` и становится `current_recognition_job_id`. `FILE_UPLOAD` отвергнут — это приём нового файла, и журнал показал бы вторую загрузку, которой не было; `FILE_REPLACE` — автозамена одного файла другим. Событие пишется на файл (`object_type = stored_file`): `before_value` — прежнее задание (`job_id`, `attempt`, `status`, `error_code`), `after_value` — новое (`job_id`, `attempt`); повторная обработка пакета версии — по событию на каждый отправленный файл, с `consolidation_id` в `after_value` | + +Остальные таблицы и enum — как в v5.20. Реализация — одна миграция в `arm-core`: только добавляет значение enum; откат при +наличии событий `FILE_REPROCESS` останавливается с ошибкой (журнал append-only). + +## Что изменилось в v5.20 (решения владельца 21.10.2026; RSHB/arm-ka-docs#147, OQ-161, AURORA-1442) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `document`: `is_modified` (not null, default `false`), `original_values` (jsonb), `modified_by` (FK → `app_user`), `modified_at` | Аналитик уточняет **период и счёт документа ОСВ**, если они распознаны неверно (`context/05-domain-rules.md` §9.5 п. 12; OQ-161, вариант 2). Новые значения пишутся **на месте** — в `period_year`, `period_quarter`, `is_cumulative`, `osv_account_code` той же строки `document`; при первой правке прежние значения этих четырёх полей копируются в `original_values` и дальше не меняются; `is_modified` — текущие отличаются от исходных. Так же устроена правка сумм `osv_entry` (`original_values`) — первичка не мутируется: файл, JSON результата в S3 и исходные значения остаются как пришли. Копия `document` (второй документ с другим `file_ordinal`) отвергнута: `file_ordinal` — номер документа в `documents[]` результата, а не номер редакции; копия задвоила бы строки ОСВ, подтверждение и привязки. Строки `osv_entry` при смене счёта не меняются (у них свои коды субсчетов) | + +Остальные таблицы и enum — как в v5.19. Реализация — одна миграция в `arm-core`: только добавляет колонки (`is_modified = +false`, остальные `NULL`); откат на данных с `is_modified = true` останавливается с ошибкой (потерялись бы исходные значения). + +## Что изменилось в v5.19 (решения владельца 17.10.2026, Р-1…Р-19; RSHB/arm-ka-docs#116) + +Change `openspec/changes/archive/2026-10-19-unified-statement-normalization`: формы ББ/ОФР нормализуются общим кодом сервисов, контракт +результата v2.0; ядро хранит результат как пришёл (`context/05-domain-rules.md` §11, §9.3, §9.5). + +| № | Изменение | Решение | +|---|---|---| +| 1 | `financial_statement_line`: снята уникальность `(meta_id, line_code)` (индекс остаётся неуникальным); новые `row_index`, `source_ref` (частичный уникальный `(meta_id, source_ref) WHERE source_ref IS NOT NULL`), `line_status` (enum `statement_line_status`: `ok` / `unreadable` / `missing` / `detail`), `line_code_text`, `parent_row_index`, `needs_review`, `is_confirmed` | Повтор кода — законный случай (один код, разные наименования; Р-4): хранятся все строки, чтения «по коду» идут через агрегат формы по коду (сумма строк `ok`, не хранится — DECISION-5). Строки без кода хранятся со статусом (Р-8, отменяет OQ-36 / OQ-141): `detail` — расшифровка под строкой с кодом, в §7, агрегаты, КФО и ЕХ не входит; `unreadable` / `missing` блокируют подтверждение, пока у них есть суммы (Р-13). Порядок и положение в источнике (`row_index`, `source_ref`) нужны экрану, чтобы показать строку и предупреждение у места. **Признак «строка создана правкой аналитика» — `row_index IS NULL`** (прежний признак `line_code_original_value IS NULL` не годится: у строк `unreadable` / `missing` / `detail` источника кода тоже нет); `line_code_original_value` — код из результата как пришёл, правками не меняется | +| 2 | `financial_statement_value`: `source_text`, `needs_review`, `is_confirmed`, `confirmed_by`, `confirmed_at` | `needs_review` **хранится** (пересматривает п. 4 v5.13): его считает сервис («подтверждение проверкой» §7 — Р-5, Р-7), ядро пересчитывает только после правок; подтверждение ячейки без правки аналитиком переживает пересчёт. Сырой текст источника — подсказка ячейки | +| 3 | `financial_statement_column`: `period_kind`, `label_confidence` | Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года») — дата колонки от эффективной даты документа, ядро досчитывает после правки даты отчёта (Р-6); колонка без даты остаётся с `period_kind = none` | +| 4 | `financial_statement_meta`: `form_variant` (enum `statement_form_variant`), `needs_review_fields`, `header_source` (jsonb), `organization_name_original_value`, `organization_name_modified` | Упрощённая форма — свой набор соотношений §7 (`NULL` — документ до v5.19 или результат 1.x, набор полный); поля шапки, требующие проверки (сейчас — неуверенная дата отчёта); сырой текст и уверенность всех полей шапки результата (`<поле>_text`, `<поле>_confidence`) — подсказка у поля на экране; наименование организации правится, как ИНН | +| 5 | Новая таблица `document_issue` + enum `document_issue_kind`, `issue_severity`, `issue_source` | Предупреждения и нарушения §7 документа с уровнем и привязкой к строкам (`line_ids`), колонке (`column_id`, а вне колонок формы — `column_index`), полю шапки; у нарушения — `codes` (коды цели и членов); как пришли от сервиса или посчитаны ядром. Очищается при перезагрузке файла; при пересчёте после правки `relation` / `soft` заменяются целиком, `warning` сервиса сохраняются, кроме снимаемых правкой. **Единственный источник нарушений** для экрана и подтверждения: для результатов 1.x ядро один раз при приёме считает §7 копией `arm_statements` и пишет записи с `source = core`; формы до миграции не пересчитываются (база очищается, Р-24). Ведётся и у документов ОСВ — предупреждения листа `OSV_*` результата 2.0 | +| 6 | `recognition_job.statements_version` | Коммит копии общего кода `arm_statements`, которым посчитан результат (`service.statements_version`); расхождение с копией ядра при пересчёте — `info` | +| 7 | `document`: уникальность `(stored_file_id, document_type, osv_account_code)` — частичная, кроме `BALANCE` / `INCOME`; новая колонка `file_ordinal` + уникальный `(stored_file_id, file_ordinal)` | Вторая форма того же типа в файле хранится (Р-10, Р-15); при двух формах одного типа и периода ни одна не автоподтверждается, подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` (Р-18). ОСВ по счёту, расшифровка и прочий — по-прежнему один на тип на файл (§9.5 п. 2) | +| 8 | `related_group.crm_id` (`varchar`, nullable, unique) | ID ГСК/ГСЗ в ЕЦП CRM из форм `51-03`, `51-04`, `77-02` (AURORA-1619, arm-core#112, OQ-33); вводится вручную, импорт ЕЦП не заполняет (OQ-91); уникален, как `crm_id` ГК и ЮЛ | + +Остальные таблицы и enum — как в v5.18. Реализация — одна миграция в `arm-core`; перед выкладкой база очищается от форм +ББ/ОФР (решение владельца 18.10.2026, Р-24), поэтому миграция только добавляет колонки, таблицу и значения по умолчанию и +§7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): `row_index` — `NULL` у строк, +созданных правкой (`line_code_original_value IS NULL`), у остальных — по возрастанию `line_code` в форме; `line_status = +ok`, `needs_review` значений — **НЕ** `values_ok`, `source_text = string_value`, `period_kind` — `absolute` при +`period_date`, иначе `none`, `header_source` — из `*_original_value`, `file_ordinal` — по `(created_at, id)` в файле; +`document_issue` пуст — у форм, оставшихся в неочищенной базе, нарушения до первой правки не показываются. Откат +гарантирован на пустой БД и на данных, которые v5.18 представляет без потери первичных данных и ручного ввода (без +повторов кода, строк без кода, двух форм одного типа в файле, `crm_id` групп и правленого наименования организации); на +таких данных `downgrade` останавливается с ошибкой, а производные колонки v5.19 и `document_issue` удаляет с записью в +журнал миграции. + +## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `stored_file.consolidation_id` (`uuid`, nullable, FK → `consolidation`) + индекс `(legal_entity_id, consolidation_id)` | Файл загружается **в консолидацию** (период ГК) и становится основой её «рабочего стола» (полный состав стола — `context/05-domain-rules.md` §9.5 п. 11), общего для всех версий периода; `NULL` — загрузка без контекста (страница ЮЛ, монозаёмщики, API). Панель «Первичные документы» в версии показывает рабочий стол консоли для ЮЛ плюс файлы с документами, привязанными к версиям консоли. Что использует конкретная версия — по-прежнему `consolidation_document`; заморозка после `VERIFIED` — привязки и `ex_snapshot`. Привязка файла к **версии** (`consolidation_file`) отвергнута: рабочие версии периода делят первичку, а новая версия после `VERIFIED` подхватывает подтверждённое (`openspec/changes/console-files/design.md`) | +| 2 | Note `recognition_job.reply_deadline` | Без изменения структуры: срок ответа OCR — 20 мин (OQ-159) | + +Остальные таблицы и enum — как в v5.17. Реализация — миграция `0006` в `arm-core` (backfill `consolidation_id` по `consolidation_document`). + +## Что изменилось в v5.17 (решение владельца 07.10.2026, пересмотр OQ-31, RSHB/arm-ka-docs#53) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `legal_entity.kpp` и `legal_entity_history.kpp` (`varchar`, nullable) | КПП юридического лица нужен (макет: колонка «КПП» на экранах `50`, `61`, поле форм `51-02`, `79`; фронтенд: «в ручке получения ЮЛ не приходит КПП»). Формат — 9 символов: 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры, иначе `INVALID_KPP`. У ИП (`entity_type = ИП`) КПП запрещён. Для ЮЛ необязателен: существующие записи и импорт без колонки остаются валидными. Уникален по-прежнему только `inn` (КПП — атрибут; сопоставление контрагентов ОСВ по ИНН не меняется). Изменение КПП — обычная правка со снимком в `legal_entity_history`. Отменяет п. 2 v5.14 («КПП не вводится») | + +Остальные таблицы и enum — как в v5.16. Реализация — миграция `0005` в `arm-core`. + +## Что изменилось в v5.16 (Срез 4, решения владельца 06.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | Enum `counterparty_match_kind` (`INN`, `NAME`, `ALIAS`, `MANUAL`, `PROPAGATED`) и колонка `osv_entry.counterparty_match_kind` (nullable) | Способ сопоставления контрагента строки ОСВ хранится. `NULL` ⇔ `counterparty_id` `NULL`; `INN` / `NAME` / `ALIAS` — шаг автоматического резолва при маппинге (ИНН; точное имя; нормализованное имя ЮЛ или алиас); `MANUAL` — ручное сопоставление аналитиком; `PROPAGATED` — автоприменение по имени в версии (DOC-221278 п. 12–13). Вычислять на чтение нельзя: после ручного связывания появляется алиас, и ручное сопоставление неотличимо от `ALIAS`. Миграция `0004` заполняет колонку по уже смапленным строкам (ИНН без пробелов = `legal_entity.inn` → `INN`; `trim(name) = legal_entity.name` → `NAME`; остальные с `counterparty_id` → `ALIAS` — приближение: ручных сопоставлений до Среза 4 не было) | +| 2 | Семантика `consolidation_document` | Без изменения структуры: строка создаётся при подтверждении документа (по документам ЮЛ — членов периметра версий «в работе») и при добавлении ЮЛ в периметр (подтверждённые подходящие документы подхватываются); удаляется при замене/удалении файла и исключении ЮЛ из периметра. Для одной (ЮЛ, `document_type`, `osv_account_code`, период, признак нарастающего) привязан один документ — самый поздний `confirmed_at` (D-5 Среза 4) | +| 3 | Семантика `consolidation_lock` | Без изменения структуры: блокировка без истечения по времени — активна, пока её не снял владелец, администратор или не перехватил другой пользователь с `CONSOLIDATION_EDIT` по причине (OQ-119) | + +Пробелы квартальной склейки и сама сводная ОСВ по-прежнему не хранятся (DECISION-5). Остальные таблицы и enum — как в v5.15 (43 таблицы; enum — 28). Реализация — миграция `0004` в `arm-core`. + +## Что изменилось в v5.15 (Срез 2c, решения владельца 05.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `osv_data_type` + `NU` (после `BU`) | Строки налогового учёта из ОСВ «БУ, НУ» хранятся в `osv_entry` как есть (OQ-45); контракт результата допускал `NU` с v1.0, ядро раньше такой строки принять не могло | +| 2 | `app_user.email` — в нижнем регистре | Без изменения структуры: нормализация при записи (seed, администрирование), поиск по `lower(email)` сохраняется, уникальность — существующий UNIQUE по `email` (OQ-59). Expression-индекс `lower(email)` не вводится: сверка ORM↔DBML не умеет expression-индексы. Миграция `0003` приводит существующие email к нижнему регистру (при дублях по регистру — падает, слияние решает человек) | +| 3 | Семантика `stored_file.status`, `recognition_job.group_schema` | Без изменения структуры: `PROCESSING` ставит inbox по ack сервиса, `PROCESSED`/`ERROR` — цикл маппинга результата (OQ-67); маппинг выполняется отдельно от inbox (OQ-69); `group_schema` заполняет маппер; «Итого» в `osv_entry` не хранится (OQ-01) | +| 4 | Индексы `osv_entry (parent_entry_id)`, `osv_entry (recognition_job_id)`, `stored_file (current_recognition_job_id)` | Индексы для удаления файла и обхода дерева строк: без них проверки FK (в том числе self-FK `parent_entry_id`) при `DELETE /files/{id}` и очистке файла перед повторным маппингом шли seq scan'ом. Замер: DELETE документа 1000 строк 8,6 с → 16 мс на 200 тыс. строк. Обычные (не unique) индексы; закрывает OQ-117 (I-1 финального ревью Среза 2c) | + +Остальные таблицы и enum — как в v5.14 (43 таблицы, 27 enum). Реализация — миграция `0003` в `arm-core`. + +## Что изменилось в v5.14 (Срез 1, решения владельца 04.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `related_group_member.kind` + индекс `(legal_entity_id, kind) WHERE is_current` | ЮЛ состоит **одновременно в одной ГСК и одной ГСЗ** (но только в одной группе каждого вида). Отменяет ANL-030 / v5.8 («только в одной ГСК или ГСЗ»). `kind` — денормализация `related_group.kind` (равенство проверяет ядро при записи, тест на рассинхрон); индекс не выразить через join, триггер не выбран. Закрывает OQ-32 | +| 2 | КПП | **Не вводится** *(отменено в v5.17: КПП введён)*. Поле есть только в макете; у `legal_entity` его нет, импорт его не читает (закрывал OQ-31 до v5.17) | +| 3 | Семантика `company_group.crm_id`, `base_regional_office_id` | Без изменения структуры: `crm_id` = «iD ГК» выгрузки ЕЦП CRM, `base_regional_office_id` = «Базовый РФ ГК в CRM»; флаги «входит в ГСК/ГСЗ» выгрузки → членство в одной неявной ГСК/ГСЗ на ГК (правила — `05-domain-rules.md` §13) | +| 4 | `audit_action` + `LEGAL_ENTITY_CHANGE`, `USER_CHANGE` | Журнал изменений ЮЛ (создание/изменение/закрытие/перевод, алиасы) и пользователей (создание/деактивация администратором) — ТЗ 8.2; в v5.13 для них не было действия (были `GROUP_CHANGE`, `MEMBERSHIP_CHANGE`, `USER_ROLE_CHANGE`) | + +Остальные таблицы и enum — как в v5.13 (43 таблицы). Реализация — миграция `0002` в `arm-core`. + +## Что изменилось в v5.13 (ревью 30.09–01.10) + +| № | Замечание | Решение | +|---|---|---| +| 1 | Вместо RBAC «в коде» — роли **и** права | `permission` + `role_permission` возвращены; права только через роли, прямых назначений пользователю нет. Исключения — заведением роли | +| 2 | `osv_entry.parent_entry_id` не нужен — храним только строки с контрагентами | **Отложено.** Открыт вопрос: хранить ли строки уровня ACCOUNT для сверки «Σ контрагентов = итог счёта»; аналитика «Парсинг ОСВ» п.11 пока говорит «все строки» | +| 3 | `osv_group_schema` не особо нужна | Убрана. Схема шапки — `recognition_job.group_schema jsonb`; тип строки — `osv_entry.group_type` | +| 4 | `financial_statement_value.needs_review` непонятен | Убран: производное от `confidence` / `is_recognized`, порог может меняться | +| 5 | В `recognition_job` не нужны и `result`, и `raw_result` | Оставлен только `result_s3_object_id`. Сырой ответ ИС Сканирование — S3 `raw/{job_id}.json` по конвенции, в БД не регистрируется | +| 6 | `stored_file.fields_modified` — что это | Счётчик «исправлено полей» с карточки документа (v5.7). Убран — `COUNT(is_modified)` по данным | +| 6-бис | `document_type` не предусматривает несколько ОСВ или ББ+ОФР в одном файле | **Разделение файл / документ**: `stored_file` — физический файл, `document` — логическая единица (1:N). Чек-лист, проверки, подтверждение, FK разобранных данных, `consolidation_document`, `ex_snapshot_item` — на `document`. Замена (`superseded_by`) — файл целиком | +| 7 | `file_type`: убрать Р7 | Убран. `xls/xlsx/xlsb → tabular-parser`, `pdf/png/jpg → scan-gateway` | + +**Принципы модели (коротко):** +- Первичка не редактируется: файлы и распознанные значения хранятся как пришли; правки аналитика — рядом (`is_modified`, `original_*`) и в аудите. +- Производные консолидации в работе (сводная ОСВ, пары ВГО, шахматка) **не хранятся** — считаются запросом. Хранятся только ручные решения (корректировки, гудвилл, подтверждение КФО клиента). +- При верификации всё замораживается в Единое хранилище (ЕХ) как самодостаточный срез, который не зависит от таблиц консолидации. +- Единственный писатель в БД — ядро `arm-core`; сервисы распознавания видят только NATS и S3. +- Права — данные (`role_permission`), ограничения по ЗО/филиалу/статусу — policy-слой поверх прав. + +--- + +## Часть 01 — Группы компаний, организации, филиалы + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `regional_office` | Филиал банка: головной офис (ГО, `is_head_office`) и региональные филиалы (РФ). Определяет зону видимости пользователя и закрепление ГК. | ← `company_group.base_regional_office_id`, `legal_entity.regional_office_id`, `app_user.regional_office_id` | ТЗ 8.1 | +| `company_group` | Группа компаний (ГК) — верхний уровень справочника клиентов. Текущее состояние (SCD4). `crm_id` = «iD ГК» из ЕЦП CRM (NULL, если создана вручную Куратором; v5.14 — семантика по выгрузке), внутренний `code`, закрепление за филиалом (`base_regional_office_id` = «Базовый РФ ГК в CRM»), флаг архива. | → `regional_office`; ← `legal_entity`, `related_group`, `consolidation` | ТЗ 6.1, Прил. 2; макет | +| `company_group_history` | Прошлые снимки ГК: каждое изменение — новая строка с `valid_from/valid_to`. | → `company_group` | ТЗ 6.10, 8.2 | +| `legal_entity` | Юридическое лицо / ИП. Натуральный ключ — ИНН; **v5.17:** `kpp` — КПП (9 символов, nullable, только у ЮЛ, не ключ). `company_group_id` = текущая ГК; **NULL = монозаёмщик**. `close_date` — закрыто/архивировано. | → `company_group`, `regional_office`; ← `related_group_member`, `stored_file`, `document`, `osv_entry.counterparty_id`, `consolidation_member` | ТЗ 6.1, Прил. 2; ЕХ 6.1 | +| `legal_entity_history` | Прошлые снимки ЮЛ + вид события (создание, изменение, переход в другую ГК, закрытие, вывод из ГК) и дата вступления в силу. | → `legal_entity` | ТЗ 8.2; макет | +| `legal_entity_alias` | Словарь написаний контрагента: как имя встречается в ОСВ и нормализованный ключ для матчинга. Пополняется при ручном сопоставлении. | → `legal_entity` | ТЗ 6.5 | +| `related_group` | ГСК или ГСЗ внутри ГК; `kind` различает. Для ГСК формируется КФО. Иерархия не поддерживается. **v5.19:** `crm_id` — ID группы в ЕЦП CRM (вручную, уникален). | → `company_group`; ← `related_group_member`, `consolidation.related_group_id` | ТЗ 6.1, Прил. 2 | +| `related_group_member` | Состав ГСК/ГСЗ — история членства (SCD2). **v5.14:** ЮЛ может одновременно входить в **одну ГСК и одну ГСЗ** (не более одной группы каждого вида): колонка `kind` = `related_group.kind` (денормализация) и частичный уникальный индекс `(legal_entity_id, kind) WHERE is_current`. Решение владельца 04.10.2026, отменяет ANL-030. | → `related_group`, `legal_entity` | ТЗ 6.1; макет [79] | + +## Часть 02 — Пользователи, роли, права + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `app_user` | Пользователь АРМ. Аутентификация внешняя (РАИСА/SSO, `external_id`). Филиал (ГО/РФ), зона ответственности (ДКБ/ДМСБ/ДМ), табельный номер. Видимость данных = права роли + филиал + ЗО. | → `regional_office`; ← `user_role`, все `*_by` поля | ТЗ 8.1, Прил. 2 | +| `role` | Роли: seed из ТЗ 8.1 (КА РФ, КА ГО, РКА, КМ, Куратор ГО, Эксперт, Администратор; `is_system`) + роли, заведённые администратором для исключений. **v5.13:** код роли в бизнес-логике не проверяется — только права. | ← `user_role`, `role_permission` | ТЗ 8.1 | +| `permission` | **v5.13.** Атомарное право «объект × действие»: `DIRECTORY/DOCUMENT/CONSOLIDATION/KFO/EX/EXPORT/USERS/AUDIT` × `VIEW/EDIT/APPROVE/VERIFY/DELETE/EXPORT/ADMIN`. Seed, привязан к коду — администратор новые коды не создаёт. Право говорит «может»; policy-слой (ЗО, филиал, статус КФО) — «эту ли и сейчас ли». | ← `role_permission` | ТЗ Прил. 2, сноска 29 | +| `role_permission` | **v5.13.** Матрица доступов как данные: `effective_permissions(user) = ∪ permissions(roles(user))`. Редактируется администратором на экране «Изменение ролей и прав доступа»; история — `audit_event(ROLE_PERMISSION_CHANGE)`. Прямых прав у пользователя нет. | → `role`, `permission` | ТЗ Прил. 2; макет | +| `user_role` | Назначение ролей пользователю (M:N). История — в аудите. | → `app_user`, `role` | ТЗ 8.1 | + +## Часть 03 — Бухгалтерские справочники и правила + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `chart_of_account` | План счетов с иерархией, тип A/P/AP, признак забалансового. Seed — полный план 1С; нестандартные субсчета клиентов добавляет администратор. | ← `osv_entry.account_id`, `elimination_rule_account`, `required_document_rule` | ТЗ 6.2/6.5, Прил. 3/4 | +| `elimination_rule` | Правило элиминации из Прил. 3 (ББ) / Прил. 4 (ОФР) как данные: пара статей, сторона, мера, режим, НДС, акцизы. Версионируется. | ← `elimination_rule_account`, `correction.rule_id`; версия фиксируется в `consolidation_version` | ТЗ 6.4, Прил. 3/4 | +| `elimination_rule_account` | Счета/субсчета каждой стороны правила, включая исключения («76 кроме 76.7.1»). | → `elimination_rule`, `chart_of_account` | ТЗ Прил. 3/4 | +| `required_document_rule` | Какие документы обязательны по подходу. **v5.13:** сверяется с `document` (тип + счёт + период), а не с файлом: выполнено = есть `document CONFIRMED` у каждого ЮЛ периметра. | → `chart_of_account` | ТЗ 5.1, 6.2, сноска 12 | + +## Часть 04 — Файлы, документы, S3, задания распознавания + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `s3_object` | Реестр всех объектов в S3: бакет-роль, ключ, размер, sha256, срок жизни, ссылка на архив в data lake. Единая точка ретеншна (5 лет по ТЗ 8.4) и дедупа. | ← `stored_file`, `recognition_job` | ТЗ 6.2, 8.4; ЕХ 8.3 | +| `stored_file` | **v5.13: только физический файл** одной загрузки: чей (ЮЛ), формат, hash, кто/когда загрузил, статус обработки (`UPLOADED → PROCESSING → PROCESSED / ERROR / SUPERSEDED`), версия при замене. **v5.18:** `consolidation_id` — консолидация, в контексте которой файл загружен (основа «рабочего стола консоли», общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11; `NULL` — без контекста). Что внутри файла — `document`. Замена — файл целиком со всеми документами. | → `legal_entity`, `consolidation`, `s3_object`, `recognition_job`; ← `document`, `recognition_job` | ТЗ 6.2, 8.2; UC-01…05 | +| `document` | **v5.13, новая.** Логический документ внутри файла: ББ, ОФР, ОСВ по счёту (`osv_account_code`), расшифровка, прочее. Период, отчётная дата, где в файле (`locator`: лист / страницы), статус проверки (`RECOGNIZED / NEEDS_CHECK / CONFIRMED / ERROR`), результаты проверок периода и контрагента, кто подтвердил. Один файл → N документов (PDF с ББ+ОФР; xlsx с несколькими ОСВ). Создаёт ядро при маппинге результата `recognition_job`. На документ ссылаются разобранные данные, чек-лист, консолидация и ЕХ. **v5.19:** `file_ordinal` — индекс документа в `documents[]` результата (у всех документов); формы ББ/ОФР одного типа в файле могут повторяться (ОСВ по счёту, расшифровка, прочий — один на тип); предупреждения и нарушения — `document_issue`. **v5.20:** период и счёт ОСВ уточняются аналитиком на месте, распознанные — в `original_values` (`is_modified`, `modified_by/at`). | → `stored_file`, `legal_entity`, `app_user`; ← `osv_entry`, `financial_statement_meta`, `document_issue`, `consolidation_document`, `ex_snapshot_item` | ТЗ 6.2, 5.1, 8.2; макет (чек-лист, «Подтвердить данные») | +| `recognition_job` | Одна попытка распознать/распарсить файл. **Единственная точка обмена с NATS** (outbox/inbox, дедуп по `result_msg_id`, DLQ). Хранит сервис и версию, **одну** ссылку на нормализованный JSON-результат в S3 (v5.13), метрики, и **схему группировок шапки ОСВ по листам** (`group_schema jsonb`, v5.13 — вместо `osv_group_schema`). **v5.19:** `statements_version` — версия копии общего кода нормализации, которой посчитан результат форм. | → `stored_file`, `s3_object`; ← `osv_entry`, `financial_statement_meta` | ТЗ 6.2, 6.10; DECISION-4 | + +## Часть 05 — Разобранные ОСВ + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `osv_entry` | Строка ОСВ: документ (счёт + период), владелец, тип строки по схеме шапки (`group_type`, v5.13), уровень и родитель (`parent_entry_id` — **открыт вопрос, замечание 2**), код счёта и резолв в план счетов, сырое имя/ИНН контрагента, резолв в ЮЛ и способ сопоставления (`counterparty_match_kind`, v5.16), шесть сумм, тип данных (БУ/валюта/кол-во), «подакцизный» для 90-го. Ручные правки: `is_modified`, `original_values`, `modified_by`. Признак ГСК/ГК/Внешний не хранится — выводится из периметра версии; способ сопоставления контрагента хранится (`NULL` ⇔ контрагент не сопоставлен). | → `document`, `recognition_job`, `legal_entity` ×2, `chart_of_account` | ТЗ 6.2/6.5, Прил. 2; «Парсинг ОСВ» | + +## Часть 06 — Распознанные формы ББ/ОФР + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `financial_statement_meta` | Шапка распознанной формы (1:1 с `document` BALANCE/INCOME): ИНН и наименование как в документе, отчётная дата, единица измерения, реквизиты. Ревью формы (`reviewed_by/at`) и акцепт КА непройденных логических проверок с комментарием. Подтверждение документа — `document.confirmed_by`. **v5.19:** `form_variant` (полная / упрощённая — набор соотношений §7), `needs_review_fields`, `header_source` (сырой текст и уверенность полей шапки), правка наименования (`organization_name_original_value`, `organization_name_modified`); результат проверок §7 и предупреждения хранятся в `document_issue` как пришли от сервиса, ядро пересчитывает их только после правок. | → `document`, `recognition_job`, `app_user`; ← `column`, `line` | ТЗ 6.2/6.3; FNR01; макет | +| `financial_statement_column` | Колонки-периоды формы с датой — нужны для межформенной проверки (1370 на начало года) и гудвилла. **v5.19:** `period_kind` (абсолютная / относительная подпись бланка / без даты), `label_confidence`. | → `meta`; ← `value` | ТЗ 6.3 п.7, 6.7 | +| `financial_statement_line` | Строка формы как в источнике: код (или его сырой текст) и наименование как распознано. **v5.19:** все строки источника — с кодом (`ok`, повторы кода тоже) и без (`unreadable`, `missing`, `detail` — расшифровка под родителем `parent_row_index`); порядок `row_index` (`NULL` ⇔ строка создана правкой аналитика), положение `source_ref`, `needs_review` неуверенного кода, подтверждение кода `is_confirmed`. Раскладка экрана — по коду «как на бумаге». | → `meta`; ← `value`, `document_issue.line_ids` | ТЗ 6.2, Прил. 2 | +| `financial_statement_value` | Число в ячейке (строка × колонка): сумма, уверенность, `is_recognized`, ручная правка с исходным значением. **v5.19:** подсветка — только `needs_review` (хранится: уверенность, `is_recognized` и §7 сведены сервисом, ядро пересчитывает после правок); `source_text` — сырой текст источника; `is_confirmed` / `confirmed_by/at` — подтверждено без правки. (v5.13 этот флаг убирала как производный.) | → `line`, `column`, `app_user` | ТЗ 6.2, Прил. 2 | +| `document_issue` | **v5.19, новая.** Предупреждение или нарушение §7 документа (у формы; у ОСВ — предупреждения листа): вид (`warning` / `relation` / `soft`), код и уровень (`error` / `info`), сообщение, привязка к строкам (`line_ids`), колонке (`column_id` или `column_index`) и полю шапки, источник (`service` / `core`); для нарушения — соотношение, `codes`, `expected`, `actual`. Экран показывает их у места. | → `document`, `financial_statement_column` | change `unified-statement-normalization` (design, шаг 8) | + +## Часть 07 — Консолидация + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `consolidation` | Логическая консолидация: **ГК** + отчётный период. ГСК фиксируется/создаётся при верификации по фактическому периметру. Уникальна по (ГК, год, квартал). **v5.18:** файлы, загруженные в консолидацию, — `stored_file.consolidation_id` (основа рабочего стола, общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11). | → `company_group`, `related_group`; ← `consolidation_version`, `stored_file` | ТЗ 6.10, 7.2 | +| `consolidation_version` | Версия консолидации: номер, статус, сценарий, подход, гудвилл, инициатор и его офис (маршрут), кто утвердил / верифицировал, решение УОБ, отклонения от Методики, версия правил элиминации, ссылка на предыдущую. После VERIFIED — read-only. | → `consolidation`, `app_user`, `regional_office`, self; ← всё в частях 07–08 | ТЗ 5.1, 6.4, 6.10; макет | +| `consolidation_status_history` | Каждый переход статуса: из → в, кто, когда, комментарий (обязателен при возврате). | → `consolidation_version`, `app_user` | ТЗ 6.10, 8.2 | +| `consolidation_lock` | «Взять в работу»: кто редактирует версию, перехват с причиной (v5.16: без истечения по времени, OQ-119). Из макета — ANL-036. | → `consolidation_version`, `app_user` | макет | +| `consolidation_member` | Периметр версии — зафиксированный список ЮЛ; `is_head_entity` для гудвилла. | → `consolidation_version`, `legal_entity` | ТЗ 6.1, 6.10 | +| `consolidation_document` | **v5.13:** какие **документы** (не файлы) участвуют в версии. Чек-лист = `required_document_rule` × периметр − `consolidation_document`. Переиспользование одного документа в нескольких версиях/периодах, монозаёмщики из ЕХ. | → `consolidation_version`, `document` | ТЗ Прил. 2; ЕХ 7 | + +*Не хранятся (DECISION-5), считаются запросом по версии:* сводная ОСВ, признак контрагента ГСК/ГК/Внешний, пары ВГО, шахматка, результирующая таблица ВГО. + +## Часть 08 — Корректировки, гудвилл, результат КФО + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `correction` | Ручная корректировка ВГО: пара (правило + компания 1 + компания 2 — естественный ключ), ББ/ОФР, строки и суммы по Дт и Кт, дочерние строки для распределения, причина, обязательный комментарий, статус, номер версии (история — в аудите). КФО = свод − Σ корректировок. | → `consolidation_version`, `elimination_rule`, `legal_entity` ×2, `chart_of_account` ×2, self | ТЗ 6.4, 6.8, Прил. 9 | +| `vgo_assignment` | Ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения по правилам ВГО: привязка к правилу / снятие привязки, «не учитывать в КФО» у нераспределённой строки. Разнесение само по себе не хранится — только это решение поверх него. | → `consolidation_version`, `osv_entry` (ON DELETE CASCADE), `elimination_rule`, `app_user` ×2 | DOC-272613 §5, AURORA-1751 | +| `goodwill` | Строка расчёта гудвилла: головная × дочерняя, доля владения, ФВ по 58.01, УК (1310) и добавочный капитал (1350 — вопрос методологам), гудвилл, «включить в КФО», комментарий. | → `consolidation_version`, `legal_entity` ×2, `app_user` | ТЗ 6.7, Прил. 3 п.2, Прил. 8 | +| `kfo_result` | Шапка результата КФО (тыс. руб.): когда посчитан, заморожен ли, ЧА и ВБ, выручка, прибыль, расхождения, пороги, обоснование, итог гудвилла, предупреждения расчёта, сводка корректировок. Одна запись на версию; пока не заморожена — пересчитывается при чтении (§8 «КФО» п. 12). | → `consolidation_version` (1:1); ← `kfo_result_line` | ТЗ 6.4, 6.8, Прил. 10 | +| `kfo_result_line` | Строка консолидированного ББ/ОФР по коду бланка редакции на отчётную дату (66н / ФСБУ 4/2023) и служебная `GOODWILL`: Σ по данным клиента − в т. ч. взаимные (авто или корректировка — не обе по одному агрегату) + гудвилл = КФО банка; расхождение, комментарий. | → `kfo_result`; ← `kfo_result_line_company` | ТЗ 6.4, Прил. 10; макет | +| `kfo_result_line_company` | Разрез строки КФО по компаниям периметра: по данным клиента, в т. ч. взаимные, очищенные, доля в показателе по группе. | → `kfo_result_line`, `legal_entity` | ЕХ 6.6; макет | +| `client_kfo_line` | Сценарий «подтверждение КФО клиента»: по строке — значение и ВГО от клиента (ручной ввод), значение и ВГО банка, расхождение, причина. | → `consolidation_version`, `app_user` | ТЗ 5.1, 6.10; макет | + +## Часть 09 — Комментарии, аудит, Единое хранилище + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `comment` | Полиморфный комментарий к объекту (`document | consolidation_version | …`); сейчас используется для документов. Правка и удаление не предусмотрены; аудит не ведётся. | → `app_user`; `object_type + object_id` без FK | ТЗ 6.3; AURORA-1446; макет | +| `audit_event` | Журнал действий (append-only): кто, когда, действие, раздел, объект, версия консолидации, before/after, результат, IP/браузер. Покрывает историю правок распознанных данных, корректировок, выгрузок, импорта справочников, входов, **изменений матрицы прав** (v5.13), подтверждения документов, **изменений ЮЛ и пользователей** (v5.14: `LEGAL_ENTITY_CHANGE`, `USER_CHANGE`). | → `app_user`, `consolidation_version` | ТЗ 8.2, Прил. 2; макет | +| `ex_snapshot` | Срез ЕХ: одна ГСК на одну отчётную дату, версия, статус (актуальный/архив), когда и кем верифицирован и записан. Самодостаточен — на консолидацию ссылается только данными. | → `related_group`, `company_group`, `app_user`, self; ← `member`, `item` | ЕХ ТЗ 6.2, 6.4, 5.3 | +| `ex_snapshot_member` | Периметр среза на дату: ЮЛ с ИНН и наименованием на момент верификации. | → `ex_snapshot`, `legal_entity` | ЕХ ТЗ 6.3 | +| `ex_snapshot_item` | Содержимое среза. Два вида: исходный документ → ссылка на **`document`** (v5.13; скачать — через его файл) и всё производное → `payload` jsonb. | → `ex_snapshot`, `document`, `legal_entity` | ЕХ ТЗ 5.2, 6.7 | + +--- + +## Как читать модель по сценарию «сформировать КФО» + +1. **Справочники** (01–03): ГК → ЮЛ → ГСК/ГСЗ; план счетов; правила элиминации; что обязательно загрузить. +2. **Создание консолидации** (07): `consolidation` от ГК + период → `consolidation_version` → `consolidation_member` (периметр). +3. **Загрузка** (04): `stored_file` + `s3_object` → `recognition_job` (queued → NATS → сервис → результат в S3 → done) → ядро создаёт **`document`** на каждый ББ / ОФР / ОСВ по счёту, найденный в файле. +4. **Разбор** (05–06): ОСВ → `osv_entry`; ББ/ОФР → `financial_statement_meta/column/line/value`. Аналитик проверяет, правит, подтверждает **документ**; логические проверки — на лету; сопоставляет контрагентов (→ `osv_entry.counterparty_id`, `legal_entity_alias`). Чек-лист закрывается по `document CONFIRMED`. +5. **Консолидация** (вычисляется): сводная ОСВ, пары ВГО, шахматка — из `osv_entry` × периметр × правила. Аналитик вносит `correction`, заполняет `goodwill`, при сценарии подтверждения — `client_kfo_line`. Результат — `kfo_result(_line)`. +6. **Согласование** (07): `consolidation_status_history` по маршруту; блокировка `consolidation_lock`. Что можно — решает `role_permission` + policy-слой по статусу/ЗО. +7. **Верификация → ЕХ** (09): `ex_snapshot` + `member` + `item` (документы по ссылке, остальное — payload). Версия → read-only. Всё это время — `audit_event`. + +## Открытые вопросы после v5.14 + +- **Замечание 2** (`osv_entry.parent_entry_id`, какие строки ОСВ хранить) — отложено; затрагивает и аналитику «Парсинг ОСВ» п.11. +- Замена по-документно (одна ОСВ из файла с пятью) — сейчас не предусмотрена, замена файлом целиком. Если понадобится — `superseded_by` переезжает в `document`. +- Статус `document.RECOGNIZED` (ручная проверка не требуется по порогу) — подтвердить с аналитиками, нужен ли, или все документы проходят через `NEEDS_CHECK → CONFIRMED`. diff --git a/openspec/changes/kfo-calculation-contract/.openspec.yaml b/openspec/changes/kfo-calculation-contract/.openspec.yaml new file mode 100644 index 0000000..8a94d76 --- /dev/null +++ b/openspec/changes/kfo-calculation-contract/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-06 diff --git a/openspec/changes/kfo-calculation-contract/design.md b/openspec/changes/kfo-calculation-contract/design.md new file mode 100644 index 0000000..c30f77c --- /dev/null +++ b/openspec/changes/kfo-calculation-contract/design.md @@ -0,0 +1,299 @@ +# Design: расчёт КФО — правила §8, модель v5.26, контракт API + +Change `kfo-calculation-contract`, issue RSHB/arm-ka-docs#206, ветка `feat/206-kfo-contract`. +Читатели результата — Цыганков (AURORA-1546, расчёт), Степанов (AURORA-1547, экран на фейке). Читают как постановку, +без созвона: документ должен отвечать на «что строить» сам. + +## 1. Зачем + +С 10.11.2026 в работу идут расчёт и экран КФО (Гант: разработка 10–16.11, тест заказчика 17–25.11). Сейчас у них нет +опоры: §8 описывает КФО по прежней редакции, в модели не хватает колонок по компании, контракта API нет. Цель — +к утру 09.11 дать три документа, по которым бэк и фронт стартуют параллельно и не расходятся. + +Успех: Цыганков пишет расчёт по §8 и DBML, не спрашивая «а как тут»; Степанов строит экран по контракту на фейковых +данных, и экран без правок подключается к настоящему бэку. + +## 2. Цели, не цели, анти-цели + +**Цели** +- §8 «КФО» — полный алгоритм от форм и агрегатов до строки КФО, расхождений и порогов, с примерами. +- DBML v5.26 — всё, что нужно хранить, чтобы отдать экран из `kfo_result` без пересчёта. +- Контракт API КФО — чтение результата и сохранение ручных полей, md + OpenAPI 3.1. + +**Не цели** — см. `proposal.md` «Non-goals» (код, ОФР-правила 1999, механизм пересчёта 1417, КФО Общий, подтверждение +клиента, экспорт, проверки переходов, новые поля версии). + +**Анти-цели** (провал, даже если документы формально готовы) +- Решения заказчика выданы за принятые. Каждая развилка из «Что решить» DOC-274636 помечается: «на старте — вариант N, + решает заказчик». +- Сумма исключается дважды (автосумма + корректировка, обычный ББ-2 + гудвилл). +- Контракт, который экран не может отрисовать без своих расчётов: фронт не считает ни долей, ни итогов, ни порогов. +- Код роли или номер правила в бизнес-логике: поведение правила задаётся данными (`elimination_mode`), не `rule_no`. + +## 3. Ограничения + +- Источник истины по полям — DBML; полей, которых нет в v5.26, в §8 и контракте нет. +- Деньги — `numeric(18,2)`, в API — строки с двумя знаками; float нет. +- Производные консолидации не хранятся (DECISION-5); `kfo_result(_line, _line_company)` — хранимый результат версии, + это разрешено моделью; агрегаты ВГО по-прежнему считаются на запрос. +- Соглашения API — как в `dev/api-contract-corrections.md` §1 и `dev/api-contract-goodwill.md` §1 (префикс, права, + 404 вне видимости, формат ошибок, порядок проверок правок, `editable` / `read_only_reason`). +- **Нумерация DBML.** В docs открыт PR #203 (v5.25, метрики OCR, AURORA-1759). Наша версия — v5.26. Если #203 слит + раньше нашего PR — строим v5.26 поверх v5.25. Если позже — v5.26 строим от v5.24 и в шапке пишем «v5.25 — PR #203»; + второй слитый PR перед merge вливает изменения первого в свой файл (номера не переписываются задним числом). +- Язык — русский, идентификаторы — английские. + +## 4. Что решено + +### 4.1 §8 «КФО» — содержание + +Блок «КФО» §8 заменяется целиком; блоки «Корректировка», «Гудвилл», «Пороги», «Сравнение с КФО клиента» правятся +только там, где противоречат новому блоку. + +**Единицы.** Результат КФО хранится и отдаётся в **тыс. руб.** с двумя знаками. Суммы ОСВ и агрегатов — в рублях, +переводятся в тысячи один раз на ячейке «в т. ч. взаимные» (компания × строка, см. «Округление»). + +**1. Строки.** Бланк ББ и ОФР — полный вариант (`full`) той редакции, которую ядро выбирает по **отчётной дате версии** +(как §11.9 и словарь имён ядра: ≤ 31.12.2024 — `66n`, позже — `fsbu4`; `contracts/statement-code-orders.yaml`), +наименования — словарь имён ядра. Для «Русского бисквита» на 01.10.2024 это 66н (1120, 1231, 2930 есть, 1105, 1215, +2420 нет). Плюс служебная строка `GOODWILL` (п. 8). Все строки бланка выводятся, даже нулевые. +Строки форм компаний вне бланка в КФО не входят (предупреждение `LINE_OUTSIDE_BLANK`), строки без кода — тоже +(решение 17.10.2026). На старте — вариант черновика (DOC-274636 вопрос 1): только бланк + гудвилл; решает заказчик. + +**2. «По данным клиента» (`client_amount`).** По каждой компании периметра (`consolidation_member`) — подтверждённый +(`CONFIRMED`) документ ББ / ОФР, привязанный к версии (`consolidation_document`). Колонка: ББ — на конец отчётного +квартала, ОФР — за период с начала года по конец квартала (по дате колонки, не по `is_current`; так же, как УК +гудвилла). Строки со статусом `ok`, повторы кода суммируются. Единицы формы — словарём единиц `arm_statements` → +тыс. руб.: «тыс. руб.» как есть, «руб.» / 1000, «млн руб.» × 1000, до двух знаков `ROUND_HALF_UP`. +**Знак строк расходов.** Формы разных компаний могут хранить расходы по-разному (§7, §11.9: скобки, минус или «без +знака»). КФО приводит строки расходов бланка — 2120, 2210, 2220, 2330, 2350 — к одной конвенции: `client_amount = +−|значение|`. Остальные строки — как в форме (1370, 2300, 2400 и прочие могут быть любого знака по существу). +- нет формы → значения компании 0, предупреждение `FORM_MISSING` (вариант 1 вопроса 3 черновика; решает заказчик); +- две и более подтверждённые формы одного типа → 0, `FORM_AMBIGUOUS`; +- единицы не распознаны → 0, `FORM_UNITS_UNKNOWN`. +КФО считается и при неполном комплекте (вопрос 17 черновика: вариант «считать, предупредить»). + +**3. Агрегаты.** Источник взаимных сумм — агрегаты таблицы корректировок (§5 п. 2a, `VgoService`): тройка (правило, +компания 1, компания 2) с суммами сторон `amount_1`, `amount_2` (рубли) после ручных решений `vgo_assignment`. +Нераспределённые строки ни в один агрегат не входят и в КФО не исключаются (предупреждение `UNALLOCATED_ROWS` с числом +строк без отметки «не учитывать в КФО»). Тип отчётности агрегата — из правила. Отдельный обход ОСВ в КФО не делается: +то, что видит аналитик в таблице корректировок, и есть вход КФО. + +**4. Исключения агрегата.** Каждый агрегат даёт список исключений `(компания, код строки, сумма в рублях ≥ 0)`. +Если у агрегата есть применённая корректировка (`correction.status = APPLIED`) — п. 5, иначе автосумма по режиму +правила (`elimination_rule.elimination_mode`): + +| Режим | Автосумма | Куда | +|---|---|---| +| `min` (ББ 3–8) | `min(amount_1, amount_2)`, если обе стороны > 0 и отклонение в допуске (§8 «Корректировка», порог как в коде, OQ-178); иначе 0 | `article_1_line` у компании 1, `article_2_line` у компании 2 | +| `min_or_full` (ББ 2, новое) | обе стороны > 0 → `min(amount_1, amount_2)`; сторона 2 = 0 → `amount_1`; сторона 1 = 0 → 0. Порога нет (DOC-274230 правило 2) | компания 1 — `article_1_line`; компания 2 — каскад по кодам `article_2_line` | +| `manual` (ББ 1) | 0 — только корректировка | — | +| режимы ОФР | по правилам AURORA-1999 (§5); до неё агрегатов ОФР нет | по правилу | + +**Каскад** (составная статья «a/b/c»): сумма снимается со строки `a` компании в пределах её доступного остатка +(«по данным клиента» строки в рублях минус уже снятое с неё другими агрегатами, но не ниже 0), остаток — со строки +`b`, затем `c`; что не уместилось — целиком на последнюю строку (DOC-274230: «если остаётся положительный остаток, он +исключается из строки 1370»). Агрегаты обходятся в порядке `rule_no`, затем ИНН компании 1, затем ИНН компании 2 — +порядок детерминирован. Каскад применяется и к составной статье в корректировке, если в ней код с «/» (в коде +корректировки код один, поэтому на практике — только автосумма правила 2). + +Изменения seed правил ББ (в AURORA-1546, `arm-core/api/seed/core/elimination_rules_v1.yaml`): правило 2 — +`elimination_mode: min_or_full`, `article_2_line: "1310/1350/1370"`; правка версии 1 на месте, без новой версии +правил (п. 6 «Decisions»). + +**Автосумма — одна функция для таблицы корректировок и КФО.** Сейчас `correction_source.py` знает только `manual` и +«остальное как `min`»; с `min_or_full` таблица корректировок показала бы правило 2 с порогом 5 % и односторонний +агрегат нулём, а КФО исключил бы иначе. Поэтому этот PR правит и §8 «Корректировка», и `dev/api-contract-corrections.md`: +у агрегата `min_or_full` `eliminated_amount` = автосумма по таблице выше, `exceeds_threshold = false` всегда, статус — +`AUTO` при обеих сторонах, `ONE_SIDED` с `eliminated_amount = amount_1` при пустой стороне 2. Код — в AURORA-1546: +автосумма агрегата выносится в одну функцию домена, её вызывают и `correction_source`, и расчёт КФО. Правило 1 остаётся `manual`: по DOC-274230 обе его суммы — у компании 2 (Кт 76.7.1 − Дт +76.7.9), а сторона агрегата устроена «компания 1 — дебет, компания 2 — кредит»; автосумма правила 1 — после +переработки seed (follow-up), сейчас только корректировкой. + +**5. Корректировка агрегата заменяет автосумму.** Применённая корректировка агрегата отменяет его автосумму целиком +и не прибавляется к ней (DOC-274636 допущение 4; DOC-272613: сохранённые корректировки не меняют суммы сторон). +Исключения: компания 1 — статьи `debit[]` (основная + дочерние), компания 2 — `credit[]`, суммы как введены. Если +заполнены обе стороны и итоги сторон разные, каждая сторона исключает **меньший** итог (ответ Лысенко п. 8, §8 +«Корректировка»): снимается со статей стороны по порядку — основная, затем дочерние в порядке ввода — до исчерпания. +Пустая сторона ничего не исключает, а другая — как введена (одностороннее правило 1, ОФР 1/2/3). + +**6. «В т. ч. взаимные» по компании (`vgo_amount`).** Σ исключений компании по строке в рублях → тыс. руб., до целых +тысяч, половина — от нуля (`ROUND_HALF_UP` по модулю), со **знаком строки**: строки расходов (2120, 2210, 2220, 2330, +2350; после приведения п. 2 они ≤ 0) получают отрицательное `vgo_amount`. Так на всех строках +одно равенство: `cleaned_amount = client_amount − vgo_amount`, исключение всегда приближает значение к нулю (при +превышении — проходит через ноль). На старте — вариант 1 вопроса 8 черновика (округление ячейки); решает заказчик. + +**7. Строка КФО.** По компании: `client_amount`, `vgo_amount`, `cleaned_amount`, `share_percent = cleaned / +bank_amount × 100` (хранится 4 знака, отдаётся 2; `bank_amount = 0` → `null`). По строке: `sum_amount = Σ client`, +`vgo_amount = Σ vgo по компаниям` (= `eliminated_auto + eliminated_manual`, где auto — от агрегатов без корректировки, +manual — от агрегатов с корректировкой; гудвилл-пары — в auto), `goodwill_adjust` (у `GOODWILL` и у итогов, куда он +входит), `bank_amount = sum_amount − vgo_amount + goodwill_adjust`. + +**Итоговые строки** (1100, 1200, 1300, 1400, 1500, 1600, 1700, 2100, 2200, 2300, 2400, 2500): «по данным клиента» — итог +из формы компании, «в т. ч. взаимные» — Σ `vgo_amount` слагаемых по формуле бланка с их знаками (1100 ← 1110…1190; +1600 ← 1100 + 1200; 1700 ← 1300 + 1400 + 1500; 2100 ← 2110 + 2120; 2200 ← 2100 + 2210 + 2220; дальше по бланку); +очищенные — разность. **Гудвилл в итогах:** `goodwill_adjust` итоговой строки = Σ `goodwill_adjust` её слагаемых, то +есть `G` у 1100 и 1600; разреза по компаниям у этой части нет, поэтому `bank_amount` итога = Σ очищенных компаний + +`goodwill_adjust`, а доли компаний у 1100/1600 считаются от `bank_amount` и в сумме дают меньше 100 % на долю гудвилла. +Проверка сходимости: `bank(1600) = bank(1700)`, если сходятся формы компаний (у ГО −ФВ, у ДО −P, гудвилл +G, ФВ − P = G). Итог КФО совпадает с подписанным клиентом минус взаимные, а не с суммой своих строк +(вариант 2 вопроса 7 черновика; решает заказчик). Пример: 10 млн исключены из 2110 «Альфы» и 2120 «Беты» — у 2100 +каждой компании `vgo` ±10 000, у строки 2100 КФО Σ `vgo` = 0, валовая прибыль группы не меняется. + +**8. Гудвилл** (DOC-259025, §8 «Гудвилл»). Ветка гудвилла идёт **по паре ГО → ДО таблицы гудвилла, независимо от +агрегатов**: для пары с `use_goodwill` (действующая, отмечена, доля и комментарий, `G > 0`) при `goodwill_enabled` у +версии — исключения ГО `1170` на ФВ (как в таблице гудвилла — все строки 58.01 и потомков пары), ДО `1310` на P (без +каскада), `G` — в строку `GOODWILL`; агрегат правила 2 той же пары (компания 1 = ГО, компания 2 = ДО), если он есть, +**не применяется** — ни автосумма, ни корректировка. Если строку 58.01 этой пары аналитик перенёс в другое правило +или отметил «не учитывать», она всё равно входит в ФВ — предупреждение `GOODWILL_ROW_REASSIGNED`: ВГО по ней могут +исключиться дважды, решает аналитик. При `G ≤ 0` или выключенном гудвилле — обычный агрегат (п. 4–5). Строка `GOODWILL` — +раздел I, сразу после 1170, без разреза по компаниям, `goodwill_adjust = bank_amount = Σ G` применимых пар в тыс. руб. +(до целых тысяч); входит в итоги 1100 и 1600; `kfo_result.goodwill_total` — то же значение. Место строки: по DOC-259025 +(R-1) — отдельная строка раздела I; макет `[31-04]` показывает «В т.ч. Гудвилл» под 1170 — расхождение в открытых +вопросах, решают методологи. + +**9. Расхождения по статьям.** По агрегатам режима `min` (ББ и ОФР): `|amount_1 − amount_2|` — на статью стороны с +большей суммой (компания 1 — `article_1_line`, компания 2 — `article_2_line`), независимо от корректировок (вариант 1 +вопроса 10 черновика; решает заказчик). Агрегаты `manual`, `min_or_full` и гудвилл-пары в расхождения не входят. По +строке — Σ в рублях → тыс. руб. до целых. `total_deviation_bb` / `total_deviation_ofr` — Σ по строкам своего отчёта. +`has_deviations` — хотя бы одна строка ≠ 0. + +**10. Пороги** (ТЗ 6.8, DOC-274636 правило 8; величины — КФО банка, тыс. руб.): +- ББ: ЧА = строка 1300 (вопрос 11, вариант FNR03), ВБ = 1600. `ЧА ≥ 10 % ВБ` → база ЧА, предел 10 % ЧА; иначе (в том + числе ЧА ≤ 0) → база ВБ, предел 1 % ВБ. +- ОФР: `2200 ≥ 0` и `2200 / 2110 ≥ 10 %` → база 2200, предел 10 % 2200; иначе → база 2110, предел 1 % 2110. `2110 = 0` + → база 2110, предел 0. +- Превышение — `total_deviation > предел` (равно — норма). Процент расхождения от базы — для сводки; база = 0 → `null`. +- Превышение хотя бы по одному отчёту → нужно обоснование (`threshold_justification`). Обязательность перед отправкой + на верификацию (вариант 2 вопроса 13) проверяет переход статуса — AURORA-2002 / 1414, не КФО. + +**11. Реквизиты формы** (ТЗ 6.4, DOC-274636 правило 9): ГК, ГСК, отчётная дата, номер и статус версии, сценарий, +подход (`approach` — пакет документов), гудвилл учтён да/нет и сумма, расхождения ББ/ОФР и превышения, обоснование, +корректировки (число агрегатов с применённой корректировкой и Σ их исключений по типу отчёта), причины отклонений от +Методики (`consolidation_version.methodology_deviations`, ввод на вкладке), решение УОБ (номер, дата — только чтение). +«Подход по Методике» и «согласовано клиентом» — не делаем до ответа заказчика (вопросы 14, 15). + +**12. Пересчёт и заморозка.** Результат хранится у версии (`kfo_result`, одна запись). Пока версия не заморожена, +чтение отдаёт результат по текущим данным версии: если входы изменились после `computed_at`, сервер пересчитывает и +сохраняет до ответа (как обнаруживать изменения — AURORA-1417). Кнопки «Рассчитать» нет (вариант 1 вопроса 16). +Комментарии строк и обоснование переносятся при пересчёте по (тип отчёта, код строки). При «Верифицирован» +`is_frozen = true` (ставит AURORA-2002) — дальше результат только читается. + +**Предупреждения** — `{code, message, legal_entity_id?, statement_type?, count?}`, не блокируют расчёт: `FORM_MISSING`, +`FORM_AMBIGUOUS`, `FORM_UNITS_UNKNOWN`, `LINE_OUTSIDE_BLANK`, `UNALLOCATED_ROWS`, `INCOME_RULES_MISSING` (до 1999 — +агрегатов ОФР нет, взаимные обороты = 0), `GOODWILL_NOT_APPLIED` (гудвилл включён, а применимых пар нет), +`GOODWILL_ROW_REASSIGNED` (п. 8). Предупреждения и сводка корректировок (п. 11) — часть результата: **хранятся в +`kfo_result`** и пишутся при расчёте, чтобы замороженная версия отдавала ровно то, что видели при верификации. + +§8 содержит сквозной пример: две компании, правила 2 (с гудвиллом и без), 6 (в допуске и с корректировкой), ОФР-пара +2110/2120, итоги 1100/1600/2100, пороги — числа сходятся до тысячи. Плюс прогон на «Русском бисквите» 01.10.2024 +(эталон `Консолидация_Группа РБ.xlsx`) — как критерий 1546, с известными расхождениями: алгоритм по ТЗ (min, порог 5 %), +а эталон исключает каждую сторону полностью (OQ-155). + +### 4.2 DBML v5.26 + +- `kfo_result_line_company`: **+ `client_amount numeric(18,2)`** («по данным клиента», тыс. руб.), + **+ `vgo_amount numeric(18,2)`** («в т. ч. взаимные», со знаком строки); `amount` — пояснение «очищенные показатели»; + `share_percent numeric(7,4)` — «4 знака хранения, в API 2». Строка `GOODWILL` разреза не имеет. +- `kfo_result_line`: пояснения по §8 — единицы тыс. руб.; `sum_amount` = Σ client; `eliminated_auto` / `eliminated_manual` + — от агрегатов без / с корректировкой, **не складываются по одному агрегату**; `vgo_amount = auto + manual`; + `goodwill_adjust` — у `GOODWILL` и итогов 1100/1600 (решение #12); `consolidated_amount = sum − vgo + goodwill_adjust`; `deviation_amount` — + п. 9; `comment` ≤ 2000, переживает пересчёт; `source_kind` — всегда `balance` (ОСВ как источник — вопрос 3). +- `kfo_result`: пояснения порогов по п. 10, `goodwill_total` по п. 8, `is_frozen` — ставит переход «Верифицирован». +- `kfo_result`: **+ `warnings jsonb`** (массив предупреждений расчёта, п. 12), **+ `corrections_count_bb int`, + `corrections_count_ofr int`, `corrections_amount_bb numeric(18,2)`, `corrections_amount_ofr numeric(18,2)`** (число + агрегатов с применённой корректировкой и Σ их исключений в тыс. руб., п. 11). +- `audit_action`: **+ `KFO_EDIT`** — сохранение ручных полей КФО (комментарии строк, обоснование порога, причины + отклонений от Методики) одним событием на запрос, `object_type = kfo_result`, `before` / `after` по изменённым + полям; раздел — `KFO` (в `audit_section` он есть; исполнение плана, R-1 ledger). Миграция enum — в 1546. +- `elimination_rule.elimination_mode`: в пояснение — значение `min_or_full` (varchar, миграция схемы не нужна). +- Шапка — changelog v5.26; `entities-v5.26.md` — копия с теми же правками; ссылки «v5.24» в `CLAUDE.md` workspace и + `openspec/config.yaml` — на v5.26. +- Миграция `arm-core` (семь колонок nullable — две в `kfo_result_line_company`, пять в `kfo_result` — и значение + `audit_action`) — в AURORA-1546. + +Разреза «расхождение по компании» не добавляем: на экране «Расхождения» — общая колонка строки. + +### 4.3 Контракт API (`dev/api-contract-kfo.md` + `.openapi.yaml`) + +Префикс `/api/v1/consolidations/versions/{version_id}`. + +| Метод и путь | Право | Ответ | Ошибки | +|---|---|---|---| +| `GET …/kfo` | `KFO_VIEW` | 200 `KfoResultOut` | 404 `NOT_FOUND` | +| `PUT …/kfo/annotations` | `CONSOLIDATION_EDIT` | 200 `KfoResultOut` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER`, 422 `UNKNOWN_LINE`, 422 pydantic | + +Чтение — по `KFO_VIEW` (право есть в seed и означает ровно это), не по `CONSOLIDATION_VIEW`, как корректировки и +гудвилл; правка — `CONSOLIDATION_EDIT` (`KFO_EDIT` в seed нет). + +`KfoResultOut`: +- `version_id`, `computed_at`, `is_frozen`, `editable`, `read_only_reason`, `units = "thousand_rub"`; +- `companies[]` — `{legal_entity_id, name, inn}` в порядке колонок (по имени); +- `requisites` — п. 11; +- `summary.balance` / `summary.income` — `{deviation_total, base_kind (NET_ASSETS | BALANCE_TOTAL | SALES_PROFIT | + REVENUE), base_amount, limit_percent, limit_amount, deviation_percent, exceeded}`; +- `justification_required`, `threshold_justification`; +- `goodwill` — `{enabled, total}`; +- `balance.lines[]`, `income.lines[]` — `KfoLineOut`: `line_code`, `line_name`, `ordinal`, `is_total`, + `is_threshold_base` (подсветка строки базы при превышении), `companies[]` `{legal_entity_id, client_amount, + vgo_amount, cleaned_amount, share_percent}` (у `GOODWILL` — пусто), `sum_amount`, `vgo_amount`, `eliminated_auto`, + `eliminated_manual`, `goodwill_adjust`, `bank_amount`, `deviation_amount`, `comment`; +- `warnings[]`. + +Деньги — строки с двумя знаками **в тыс. руб.** (в отличие от корректировок и гудвилла, где рубли: результат КФО по +определению в тысячах; поле `units` явно). Переключатели «Единицы» (тыс. / млн) и «Знаков после запятой» — только показ +на фронте. + +`PUT …/kfo/annotations` — тело `{comments?: [{statement_type, line_code, comment | null}], threshold_justification?, +methodology_deviations?}`; отсутствующее поле не меняется, `null` — очищает; комментарий ≤ 2000, обоснование и причины +≤ 4000; строки нет в бланке — 422 `UNKNOWN_LINE`. Сохраняет набором в одной транзакции, аудит — `KFO_EDIT` (4.2). +Ответ — полный `KfoResultOut`. + +Состояния экрана из DOC-274647 выражаются полями ответа: нет компаний / нет форм — `warnings`; заморожено — `is_frozen` ++ `editable = false`. Примеры ответа (обычный, с превышением и гудвиллом, замороженный) — в md и в `examples` +OpenAPI, на числах сквозного примера §8; Степанов берёт их как фейк. + +### 4.4 Вложения в Jira + +После открытия PR (не дожидаясь merge): AURORA-1546 — `05-domain-rules.md` (§8) и `data-model-v5.26.dbml`; +AURORA-1547 — `api-contract-kfo.md` и `api-contract-kfo.openapi.yaml`; в обеих — комментарий со ссылкой на PR. После +правок по ревью — новые версии вложений. + +## 5. Что остаётся исполнителю плана + +Точный перечень и порядок строк бланка (из словаря ядра и `statement-code-orders.yaml`), тексты предупреждений, +формулировки §8, разметка OpenAPI (имена схем, `examples`), числа сквозного примера, расстановка открытых вопросов в +`07-open-questions.md` (новые OQ с номерами по порядку). + +## 6. Decisions made without discussion + +| # | Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---|---| +| 1 | 06.11.2026 | Развилки «решает заказчик» из DOC-274636 закрываются вариантом черновика как стартовым, с пометкой | Раскладка спринта 4, R-2; иначе 1546 не стартует | Доработка по ответу заказчика | +| 2 | 06.11.2026 | Вход КФО — агрегаты таблицы корректировок (§5 п. 2a), не отдельный обход ОСВ | Одна истина для аналитика и КФО; двойной учёт уже снят разнесением | Если заказчик захочет КФО «мимо» разнесения — переделка источника | +| 3 | 06.11.2026 | Корректировка агрегата заменяет автосумму; при разных итогах сторон каждая исключает меньший | DOC-274636 доп. 4, ответ Лысенко п. 8 | Неверная сумма ВГО на агрегатах с корректировкой | +| 4 | 06.11.2026 | Правило 2 считается автоматически по DOC-274230 (режим `min_or_full`, каскад 1310 → 1350 → 1370) правкой seed версии 1 на месте | Описание 1546 (владелец, 06.11) требует каскада; верифицированных версий нет — объяснимость старых результатов не страдает | Версии, созданные до правки, начнут считать правило 2 по-новому; если где-то это недопустимо — нужна версия правил 2 | +| 5 | 06.11.2026 | Правило 1 остаётся ручным (только корректировка) | Обе суммы DOC-274230 — у компании 2, сторона агрегата устроена иначе; переделка seed не влезает | ВГО по 76.7.x не исключаются без корректировки аналитика | +| 6 | 06.11.2026 | Результат КФО в API — в тыс. руб. (поле `units`), не в рублях, как в других контрактах | Округление до тысяч — часть расчёта; рубли из результата не восстановить | Фронт, привыкший к рублям, неверно масштабирует — снимается полем `units` | +| 7 | 06.11.2026 | `vgo_amount` со знаком строки; `cleaned = client − vgo` на всех строках | Расходы в формах с минусом; одно равенство без исключений для экрана и итогов | Если формы хранят расходы положительными — знак у 2120/2210/… перевернётся (проверить на фикстурах в 1546) | +| 8 | 06.11.2026 | Строка `GOODWILL` — отдельная строка раздела I после 1170, без разреза по компаниям, входит в 1100/1600 | DOC-259025 важнее макета (R-1); разрез по компаниям гудвиллу не определён | Если методологи выберут «в т. ч. под 1170» — перенос строки и пересчёт итогов | +| 9 | 06.11.2026 | Чтение пересчитывает устаревший результат сам; кнопки «Рассчитать» нет | Вариант 1 вопроса 16; фронту не нужно знать про устаревание | Долгий GET на больших версиях — оптимизация в 1417 | +| 10 | 06.11.2026 | Разреза «расхождение по компании» в модели нет (в чате обещал добавить) | На экране «Расхождения» — общая колонка строки | Понадобится расшифровка расхождения по компании — новая колонка | +| 11 | 06.11.2026 | Порядок обхода агрегатов для каскада — `rule_no`, ИНН компании 1, ИНН компании 2 | Детерминированный результат при нескольких агрегатах на одну строку | Иное распределение между 1310/1350/1370 при нехватке остатка | +| 12 | 06.11.2026 | Гудвилл в итогах — `goodwill_adjust` у 1100/1600 = Σ слагаемых (вопрос ревью 1, вариант «а») | Одна формула `bank = sum − vgo + goodwill_adjust` на всех строках; 1600 сходится с 1700 | Доли компаний у 1100/1600 в сумме < 100 % — если смутит заказчика, показывать долю гудвилла отдельно | +| 13 | 06.11.2026 | Строки расходов 2120, 2210, 2220, 2330, 2350 приводятся к `−|x|` независимо от знака в форме | Формы хранят знак по-разному (§7, §11.9); без единой конвенции Σ по компаниям и итоги неверны | Законно положительное значение строки расходов (сторно) станет отрицательным | +| 14 | 06.11.2026 | Автосумма агрегата — одна функция для таблицы корректировок и КФО; PR правит §8 «Корректировка» и контракт корректировок под `min_or_full` | Иначе аналитик видит одно, а КФО исключает другое | Шире PR и 1546 (правка `correction_source.py`) | +| 15 | 06.11.2026 | Предупреждения и сводка корректировок хранятся в `kfo_result` (`warnings jsonb`, 4 колонки) | Замороженная версия должна отдавать то, что видели при верификации | Лишние колонки, если ЕХ всё равно заморозит ответ целиком в срез | +| 16 | 06.11.2026 | Аудит ручных полей КФО — новое действие `KFO_EDIT` | В `audit_action` подходящего нет; инвариант «append-only аудит» | Если заказчику нужен отдельный раздел журнала — новое значение раздела | +| 17 | 06.11.2026 | Бланк — по редакции на отчётную дату версии (66н ≤ 2024, ФСБУ 4 позже) | Иначе КФО за 2024 теряет статьи 66н; так же выбирает редакцию ядро | Версия на стыке редакций с формами разных редакций — строки вне бланка в предупреждения | +| 18 | 06.11.2026 | Гудвилл-ветка — по паре таблицы гудвилла, независимо от агрегатов; агрегат правила 2 пары подавляется; перенос строки 58.01 — предупреждение | ФВ гудвилла уже определён по всем строкам 58.01 пары (контракт гудвилла) | Двойное исключение строки, перенесённой аналитиком в другое правило, — видно по предупреждению | +| 19 | 06.11.2026 | Чтение КФО — по `KFO_VIEW`, правка — по `CONSOLIDATION_EDIT` | `KFO_VIEW` в seed есть и означает ровно это; `KFO_EDIT` нет | Роль с `CONSOLIDATION_VIEW` без `KFO_VIEW` не увидит вкладку — так и задумано матрицей | +| 20 | 06.11.2026 | У итоговых строк `deviation_amount = null` | Сумма расхождений актива и пассива в итоге бессмысленна; сводка считается по строкам бланка | Экрану нужна сумма раздела — добавить на фронте (ledger, Task 1) | +| 21 | 06.11.2026 | Примеры контракта: гудвилл + превышение, гудвилл выключен, заморожено; примера «норма» нет | Все три — одна версия сквозного примера, в ней превышение | У фронта нет зелёной сводки в фейке — подменить `exceeded` руками (ledger, Task 1) | +| 22 | 06.11.2026 | `KFO_EDIT` — раздел журнала `KFO`, не `CONSOLIDATION` | `audit_section.KFO` в модели есть и точнее | Фильтр журнала «Консолидация» не покажет правки КФО (ledger, Task 2) | +| 23 | 06.11.2026 | OpenAPI КФО генерируется `scripts/kfo_contract/gen_kfo_openapi.py` из `kfo_example.py`, руками не правится | Числа примеров совпадают с §8; проверки правил в одном месте | Правка руками разойдётся с генератором при следующей перегенерации (ledger, Task 3; ревью M-9) | +| 24 | 06.11.2026 | Порядок исключений: гудвилл-пары, затем агрегаты; остаток для каскада уменьшают все исключения | Ревью I-1: иначе распределение 1310/1350/1370 у ДО с несколькими ГО зависит от реализации | Иное распределение между 1310/1350/1370, если методологи считают иначе | +| 25 | 06.11.2026 | Округляется одна ячейка: `vgo` и `manual` — до тысяч, `auto = vgo − manual` | Ревью I-2: раздельное округление ломает `vgo = auto + manual` | `auto` может отличаться на 1 тыс. от «своей» суммы в рублях | +| 26 | 06.11.2026 | `min_or_full` при `amount_2 ≤ 0` — как пустая сторона (`amount_1`), при `amount_1 ≤ 0` — 0; расхождения — по всем агрегатам `min`, в том числе односторонним | Ревью I-3; односторонний остаток — тоже неисключённое ВГО (вариант 1 OQ-186) | Порог срабатывает чаще из-за односторонних агрегатов | +| 27 | 06.11.2026 | `share_percent` при `|доля| ≥ 1000 %` — `null` (колонка `numeric(7,4)` не расширяется) | Ревью I-4: при почти нулевом итоге доля бессмысленна и переполняет колонку | Экран не покажет экстремальные доли | +| 28 | 06.11.2026 | Итоги — слагаемые с +1 после приведения знака; формулы §7 с минусом к КФО не применяются; 2410/2411 — как в форме (OQ-12); итога нет в форме (упрощённая) — Σ слагаемых | Ревью I-5, I-6 | Σ строки 2410 по компаниям с разной конвенцией знака неверна (на итоги не влияет — итоги из формы) | +| 29 | 06.11.2026 | Корректировка агрегата правила 2, закрытого гудвилл-парой, не применяется и не входит в сводку; предупреждение `CORRECTION_SUPERSEDED_BY_GOODWILL` | Ревью M-5: аналитик видит её в таблице корректировок, а КФО игнорирует | Лишнее предупреждение, если аналитик ввёл её намеренно | diff --git a/openspec/changes/kfo-calculation-contract/plans/2026-11-06-kfo-contract.md b/openspec/changes/kfo-calculation-contract/plans/2026-11-06-kfo-contract.md new file mode 100644 index 0000000..9bd16e6 --- /dev/null +++ b/openspec/changes/kfo-calculation-contract/plans/2026-11-06-kfo-contract.md @@ -0,0 +1,203 @@ +# КФО: §8, DBML v5.26, контракт API — план + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** к утру 09.11.2026 дать Цыганкову (AURORA-1546) и Степанову (AURORA-1547) правила §8 «КФО», модель v5.26 и +контракт API КФО, по которым расчёт и экран строятся параллельно. + +**Architecture:** только документы репозитория `arm-ka-docs`. §8 — алгоритм; DBML — что хранится; контракт — что +отдаёт и принимает API; OpenSpec-delta — требования в форме SHALL/сценариев. Числа сквозного примера §8 и `examples` +контракта — одни и те же, считаются одним скриптом в scratchpad, чтобы не разойтись. + +**Tech Stack:** Markdown, DBML, OpenAPI 3.1 (YAML), OpenSpec 1.13.1, `make check`. + +**Spec:** `openspec/changes/kfo-calculation-contract/design.md`, `proposal.md`. + +Все пути — от корня worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/kfo-contract/docs`; команды — из него. + +## Global Constraints + +- Результат КФО — тыс. руб., строки с двумя знаками; ОСВ и агрегаты — рубли; float нет. +- `cleaned_amount = client_amount − vgo_amount`; `bank_amount = sum_amount − vgo_amount + goodwill_adjust` — на всех строках. +- Строки расходов, приводимые к `−|x|`: 2120, 2210, 2220, 2330, 2350. +- Новый режим правила: `min_or_full`; правило 2 seed v1: `article_2_line: "1310/1350/1370"`. +- Новые поля: `kfo_result_line_company.client_amount`, `.vgo_amount`; `kfo_result.warnings`, `corrections_count_bb`, + `corrections_count_ofr`, `corrections_amount_bb`, `corrections_amount_ofr`; `audit_action.KFO_EDIT`. +- Коды предупреждений: `FORM_MISSING`, `FORM_AMBIGUOUS`, `FORM_UNITS_UNKNOWN`, `LINE_OUTSIDE_BLANK`, `UNALLOCATED_ROWS`, + `INCOME_RULES_MISSING`, `GOODWILL_NOT_APPLIED`, `GOODWILL_ROW_REASSIGNED`. +- Эндпоинты: `GET /api/v1/consolidations/versions/{version_id}/kfo` (`KFO_VIEW`), + `PUT …/kfo/annotations` (`CONSOLIDATION_EDIT`); ошибка `UNKNOWN_LINE`; `units = "thousand_rub"`. +- Новые открытые вопросы — с OQ-180 (177 и 179 заняты открытыми PR #195 и #203). +- Версия модели — v5.26; v5.25 — PR #203 (если слит до нас — строим поверх него, см. design §3). +- `docs/source/**` не трогать. Язык — русский; маркеры решений — `R-n` с номером design-таблицы. + +## Review Focus + +1. Агрегат с корректировкой, у которого и автосумма > 0 — исключение ровно одно (корректировка), не сумма двух. +2. Пара гудвилла с `G ≤ 0` или выключенным `goodwill_enabled` — обычный агрегат правила 2, строка `GOODWILL` = 0. +3. Расходная строка 2120 положительной в форме одной компании и отрицательной в другой — после приведения Σ по 2120 и + итог 2100 сходятся. +4. Каскад правила 2 при нехватке остатка 1310 и 1350 — остаток целиком на 1370, Σ исключений ДО = исключению ГО 1170. +5. Версия без форм у части компаний — КФО считается, компания с нулями, `FORM_MISSING` в `warnings`. + +Каждый пункт — числом в сквозном примере §8 (Task 1) и в одном из `examples` контракта (Task 3). + +--- + +### Task 1: §8 «КФО», §8 «Корректировка», открытые вопросы + +**Files:** +- Modify: `context/05-domain-rules.md` — §8: блок «КФО» заменить; блоки «Корректировка», «Гудвилл» («КФО» в конце), + «Пороги» — привести; §5 п. 1 — строка о режиме `min_or_full`. +- Modify: `context/07-open-questions.md` — OQ-180…: открытые вопросы КФО. +- Create (scratchpad, не в репо): `/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/38dca83c-0de5-4c8e-b11b-50b6f660f3f7/scratchpad/kfo_example.py` + +**Interfaces:** +- Produces: числа сквозного примера (компании «Альфа», «Бета», «Гамма»; таблица исключений, строки 1170, 1230, 1310, + 1350, 1370, 1520, 1100, 1300, 1600, 1700, `GOODWILL`, 2110, 2120, 2100; пороги) — Task 3 копирует их в `examples`. + +- [ ] **Step 1: Скрипт сквозного примера.** `kfo_example.py` на `Decimal`: три компании, агрегаты — правило 6 + («Альфа» → «Бета» в допуске 2 %; «Бета» → «Альфа» 20 % с корректировкой, у которой автосумма была бы 0, и ещё один + агрегат 6 в допуске 3 % с корректировкой, где итоги сторон 100/90 — проверка «меньшего»); правило 2 «Альфа» → «Бета» + с гудвиллом (ФВ, УК, d, P, G > 0); правило 2 «Альфа» → «Гамма» без пары на стороне 2 (`min_or_full` → `amount_1`) + с нехваткой 1310/1350 — каскад на 1370; ОФР-агрегат 2110 «Альфа» / 2120 «Бета» (у «Беты» 2120 положительное в форме); + «Гамма» без ОФР — `FORM_MISSING`. Скрипт печатает таблицы строк по компаниям и итоги, пороги ББ/ОФР. + Assert внутри скрипта: `bank(1600) == bank(1700)`; Σ исключений ДО по правилу 2 = исключению ГО 1170; Σ `vgo` + строки 2100 = 0; у агрегата с корректировкой в `vgo` нет автосуммы. + +- [ ] **Step 2: Прогнать скрипт.** Run: `python3 -I /kfo_example.py` — Expected: таблицы печатаются, ни + одного `AssertionError`. + +- [ ] **Step 3: Переписать блок «КФО» §8** по design §4.1 п. 1–12 (единицы, строки и редакция бланка, «по данным + клиента» и знак расходов, агрегаты, таблица режимов и каскад, корректировка вместо автосуммы, `vgo` и округление, + строка и итоги, гудвилл, расхождения, пороги, реквизиты, пересчёт и заморозка, предупреждения и их хранение), со + ссылками на источники (DOC-274230, DOC-259025, DOC-272613, DOC-274636) и пометками «на старте — вариант N, решает + заказчик (OQ-nnn)». Сквозной пример — числами из Step 2. Отдельным абзацем — прогон на РБ 01.10.2024 (66н) и + ожидаемое расхождение с эталоном по OQ-155. + +- [ ] **Step 4: Привести соседние блоки §8 и §5.** «Корректировка»: агрегат `min_or_full` — `eliminated_amount`, + `exceeds_threshold = false`, статусы `AUTO` / `ONE_SIDED`; одна функция автосуммы для таблицы корректировок и КФО; + «в КФО по выбранным статьям уходит меньшая сумма» — раскрыть по design п. 5. «Гудвилл», абзац «КФО» — ссылка на + новый блок, ветка по паре независимо от агрегата, `GOODWILL` в 1100/1600. «Пороги» — заменить ссылкой на новый блок + (дубли формул убрать). §5 п. 1 — `mode = min_or_full` в псевдокоде. + +- [ ] **Step 5: OQ-180… в `07-open-questions.md`.** По одному вопросу на развилку design, которую решает заказчик: + строки вне бланка; состав колонок; значение без формы; итоговая строка; округление; место строки гудвилла; + совокупный объём расхождений; ЧА — 1300 или расчёт; нулевая база порога; действие при превышении; «подход» по + Методике; согласование клиентом; пересчёт автоматически или по кнопке; правило 1 автоматически (follow-up seed). В + каждом — источник, «допущение принято: вариант …», ссылка на §8. + +- [ ] **Step 6: Проверка.** Run: `grep -n "eliminated_auto (Σ сумм элиминации троек" context/05-domain-rules.md` — + Expected: пусто (старая формула удалена). Run: `grep -c "OQ-18[0-9]" context/07-open-questions.md` — Expected: ≥ 10. + +- [ ] **Step 7: Commit** + +```bash +git add context/05-domain-rules.md context/07-open-questions.md +git commit -m "КФО: §8 под DOC-274230/259025/272613, открытые вопросы OQ-180… (#206)" +``` + +### Task 2: DBML v5.26 и ссылки на версию + +**Files:** +- Create: `model/data-model-v5.26.dbml` (копия v5.24 или v5.25 — см. Global Constraints), `model/entities-v5.26.md`. +- Modify: `Makefile:9`, `openspec/config.yaml:4,9`, `context/00-workspace-layout.md`, `context/03-architecture.md`, + `context/04-data-model-guide.md`, `context/08-source-docs-index.md`, `context/KICKOFF-PROMPT.md`, + `repo-templates/arm-core.CLAUDE.md`, `analyst/eliminations-vgo.md` — ссылки v5.24 → v5.26, где речь о текущей версии. +- Modify (если `npx` есть): `model/diagrams/part08_corrections_kfo.{svg,png}` через `make diagrams`. + +**Interfaces:** +- Consumes: формулировки полей из Task 1 (§8). +- Produces: имена колонок и enum — ровно как в Global Constraints; Task 3 на них ссылается. + +- [ ] **Step 1: Скопировать модель и записать changelog v5.26** в шапку: 1) `kfo_result_line_company` + `client_amount`, + `vgo_amount`; 2) `kfo_result` + `warnings jsonb`, четыре поля сводки корректировок; 3) `audit_action.KFO_EDIT`; + 4) `elimination_mode` — значение `min_or_full`; 5) пояснения `kfo_result_line` по §8 (без изменения структуры); + 6) миграция — одна в arm-core, AURORA-1546, все новые колонки nullable, откат удаляет колонки, `KFO_EDIT` при + наличии событий не откатывается (как `FILE_REPROCESS` в v5.21). + +- [ ] **Step 2: Правки таблиц и enum** по design §4.2 — `note` каждого поля ссылается на §8 п. N. + +- [ ] **Step 3: `entities-v5.26.md`** — копия с правками разделов `kfo_result`, `kfo_result_line`, + `kfo_result_line_company`, `audit_event`, `elimination_rule`; шапка — v5.26. + +- [ ] **Step 4: Ссылки на текущую версию** в файлах из списка; в `04-data-model-guide.md` — абзац «v5.26». + +- [ ] **Step 5: Проверка.** Run: `grep -rn "v5\.24" Makefile openspec/config.yaml context/00-workspace-layout.md context/04-data-model-guide.md` + — Expected: только исторические упоминания («v5.24: таблица vgo_assignment»), не «текущая». Run: + `grep -n "client_amount\|KFO_EDIT\|min_or_full\|corrections_count_bb" model/data-model-v5.26.dbml` — Expected: все + четыре найдены. Если есть `npx`: `make diagrams` — Expected: без ошибок, diff `part08_*`. + +- [ ] **Step 6: Commit** + +```bash +git add model/ Makefile openspec/config.yaml context/ repo-templates/ analyst/ +git commit -m "DBML v5.26: разрез КФО по компании, предупреждения и аудит КФО, режим min_or_full (#206)" +``` + +### Task 3: Контракт API КФО и правка контракта корректировок + +**Files:** +- Create: `dev/api-contract-kfo.md`, `dev/api-contract-kfo.openapi.yaml`. +- Modify: `dev/api-contract-corrections.md` — агрегат `min_or_full`; `dev/frontend-api-guide.md` — строка-ссылка на + контракт КФО в оглавлении контрактов. + +**Interfaces:** +- Consumes: числа Task 1 Step 2; имена полей Task 2. +- Produces: схемы `KfoResultOut`, `KfoLineOut`, `KfoLineCompanyOut`, `KfoSummaryOut`, `KfoRequisitesOut`, + `KfoWarningOut`, `KfoAnnotationsIn`, `KfoCommentIn` — их берёт 1547 как фейк и 1546 как схему ответа. + +- [ ] **Step 1: `api-contract-kfo.md`** — структура как у `api-contract-goodwill.md`: 1 «Общее» (права `KFO_VIEW` / + `CONSOLIDATION_EDIT`, тыс. руб. и `units`, порядок проверок правок), 2 «Понятия» (кратко со ссылками на §8 п. N, без + повторения формул), 3 «Эндпоинты» (таблица design §4.3, `GET` и `PUT` подробно: пересчёт при чтении, заморозка, + `null` очищает, лимиты 2000/4000, `UNKNOWN_LINE`, аудит `KFO_EDIT`), 4 «Схемы» (поля с типами и смыслом), 5 «Состояния + экрана DOC-274647 → поля ответа», 6 «Примеры» — три ответа (обычный; превышение порога ББ с гудвиллом; замороженный), + сокращённые до строк сквозного примера, 7 «Открытые вопросы» — ссылки на OQ-180…. + +- [ ] **Step 2: `api-contract-kfo.openapi.yaml`** — OpenAPI 3.1, два пути, схемы из Interfaces, `examples` из Step 1 + (полные JSON), коды ошибок как в design §4.3; деньги — `type: string, pattern: '^-?\d{1,16}\.\d{2}$'`, `nullable` + через `type: [string, "null"]`. + +- [ ] **Step 3: Валидация OpenAPI.** Run: + `uv run --with openapi-spec-validator python -I -c "from openapi_spec_validator import validate; from openapi_spec_validator.readers import read_from_filename; validate(read_from_filename('dev/api-contract-kfo.openapi.yaml')[0]); print('ok')"` + — Expected: `ok`. Плюс: каждый `example` проходит свою схему — тот же запуск с `jsonschema` по + `components.schemas.KfoResultOut` (скрипт в scratchpad) — Expected: `ok` для трёх примеров. + +- [ ] **Step 4: Контракт корректировок.** В `api-contract-corrections.md` §2/§3.1: для правила с `elimination_mode = + min_or_full` — `eliminated_amount` по §8, `exceeds_threshold = false`, статус `AUTO` / `ONE_SIDED`; сноска «изменение + кода — AURORA-1546». Проверить, что в `api-contract-corrections.openapi.yaml` описание поля `status` / `eliminated_amount` + не противоречит (поправить описание, не схему). + +- [ ] **Step 5: Commit** + +```bash +git add dev/ +git commit -m "КФО: контракт API (dev/api-contract-kfo.*), агрегат min_or_full в контракте корректировок (#206)" +``` + +### Task 4: OpenSpec-delta, проверка, PR, вложения в Jira + +**Files:** +- Create: `openspec/changes/kfo-calculation-contract/specs/arm-core/spec.md`, `openspec/changes/kfo-calculation-contract/tasks.md`. + +- [ ] **Step 1: Spec delta** — `## ADDED Requirements` в capability `arm-core`: «Расчёт КФО версии» (сценарии: агрегат + с корректировкой; гудвилл G > 0 и G ≤ 0; каскад правила 2; форма отсутствует; приведение знака расходов), + «Чтение результата КФО» (пересчёт при чтении, заморозка), «Ручные поля КФО» (сохранение, `null` очищает, + `UNKNOWN_LINE`, аудит `KFO_EDIT`), «Автосумма агрегата min_or_full» (таблица корректировок и КФО совпадают). + `tasks.md` — четыре задачи этого плана с отметками. + +- [ ] **Step 2: Проверка репозитория.** Run: `make validate-openspec CHANGE=kfo-calculation-contract` — Expected: valid. + Run: `make check` — Expected: все цели зелёные. + +- [ ] **Step 3: Финальное ревью ветки** (сабагент opus, только чтение): §8 ↔ DBML ↔ контракт — одни имена и числа; + Review Focus 1–5 покрыты; ни одной развилки заказчика без пометки. Находки `C-n/I-n/M-n` — исправить или + `deferred` в design.md. + +- [ ] **Step 4: Push и PR** — `git push -u origin feat/206-kfo-contract`; `tea pulls create --login home --repo + RSHB/arm-ka-docs --head feat/206-kfo-contract --base main` с описанием: что и зачем, Closes #206, порядок с #203, + «Rulings» (design §6), таблица находок. Merge — только по команде владельца. + +- [ ] **Step 5: Вложения в Jira** — пакет `batch_create`: AURORA-1546 — `context/05-domain-rules.md`, + `model/data-model-v5.26.dbml`; AURORA-1547 — `dev/api-contract-kfo.md`, `dev/api-contract-kfo.openapi.yaml`; + в обе — комментарий голосом владельца со ссылкой на PR (что смотреть: §8 «КФО» / контракт, открытые вопросы OQ-180…). + Expected: все команды пакета `done`. diff --git a/openspec/changes/kfo-calculation-contract/proposal.md b/openspec/changes/kfo-calculation-contract/proposal.md new file mode 100644 index 0000000..0b2f4f2 --- /dev/null +++ b/openspec/changes/kfo-calculation-contract/proposal.md @@ -0,0 +1,48 @@ +# Proposal: расчёт КФО — правила §8, модель v5.26, контракт API (docs #206) + +## Why + +Разработка КФО стартует 10.11.2026: AURORA-1546 (расчёт, Цыганков) и AURORA-1547 (экран, Степанов, на фейковых данных +по контракту). Обе задачи в «Нужно до старта» ссылаются на три документа, которых нет или которые устарели: + +- §8 «КФО» в `context/05-domain-rules.md` написан по прежней редакции: корректировки складываются с автоэлиминацией, + нет распределения по строкам правил 1 и 2, нет ветки гудвилла в расчёте, нет единиц и округления; +- в модели `kfo_result_line_company` нет «по данным клиента» и «в т. ч. взаимные» по компании — колонки экрана из + сохранённого результата не восстановить; +- контракта API КФО нет совсем. + +Источники: страницы Лысенко — DOC-274230 «Правила пар счетов» (27.10), DOC-259025 «Гудвилл» (03.11), DOC-272613 +«Корректировка данных агрегатом» (04.11); черновики команды — DOC-274636 «Расчёт КФО», DOC-274647 «Экран: КФО»; +макет `[31-04]`. При расхождении страница Лысенко важнее черновика (решение владельца 28.10.2026, распространено на КФО +в раскладке спринта 4, R-1). + +## What changes + +- `context/05-domain-rules.md` §8, блок «КФО» — переписан: источники значений, порядок расчёта, автосумма агрегата по + каждому правилу ББ, распределение по строкам, корректировка агрегата вместо автосуммы, гудвилл, ОФР, итоговые строки, + единицы и округление, расхождения и пороги, реквизиты, пересчёт и заморозка, предупреждения. +- `model/data-model-v5.26.dbml` + `model/entities-v5.26.md` — `kfo_result_line_company` получает `client_amount` + и `vgo_amount`; пояснения `kfo_result_line` (`vgo_amount`, `eliminated_manual`, `GOODWILL`, единицы) и `kfo_result` + приведены к §8; changelog в шапке. +- `dev/api-contract-kfo.md` + `dev/api-contract-kfo.openapi.yaml` — новый контракт: чтение результата КФО версии, + сохранение ручных полей (комментарии строк, обоснование порога, причины отклонений от Методики). +- §8 «Корректировка» и `dev/api-contract-corrections.md` — автосумма агрегата нового режима `min_or_full` (правило 2): + таблица корректировок и КФО считают её одной функцией. +- DBML v5.26 также: `kfo_result.warnings` и сводка корректировок, `audit_action.KFO_EDIT`, пояснение `elimination_mode`. +- `context/07-open-questions.md` — открытые вопросы КФО с вариантом, принятым на старте. + +Срез плана: Срез 6 (`09-delivery-plan.md`), Гант 2.5 / 2.8. Дочерние change'и — в `arm-core` (AURORA-1546, 1547, +1417), заводят исполнители. + +Схема среза ЕХ `ex-payload-v1` — отдельный change и PR (под AURORA-1560), строится на форме ответа этого контракта. + +## Non-goals + +- Код расчёта и экрана — AURORA-1546, 1547 в `arm-core`. +- Правила пар ОФР как данные и их расчёт — AURORA-1999 (§5); здесь только как КФО читает агрегаты ОФР. +- Механизм пересчёта и инвалидации (события, триггеры) — AURORA-1417; здесь только наблюдаемое поведение для экрана. +- «КФО Общий» (динамика), «Подтверждение КФО клиента», выгрузка формы 2.03.01.01с — AURORA-2003, 1549/1550, 1565. +- Проверки перед переходами статусов (обязательность обоснования при отправке на верификацию) — AURORA-2002 / 1414; + здесь только признак «нужно обоснование». +- Новые поля версии под «подход по Методике» и «согласовано клиентом» — ждут ответа заказчика (DOC-274636, вопросы 14, 15). +- Схема `ex-payload-v1` — следующий change. diff --git a/openspec/changes/kfo-calculation-contract/specs/arm-core/spec.md b/openspec/changes/kfo-calculation-contract/specs/arm-core/spec.md new file mode 100644 index 0000000..afc42f1 --- /dev/null +++ b/openspec/changes/kfo-calculation-contract/specs/arm-core/spec.md @@ -0,0 +1,85 @@ +## ADDED Requirements + +### Requirement: Расчёт КФО версии +Ядро SHALL считать результат КФО версии консолидации по `context/05-domain-rules.md` §8 «КФО» и хранить его в +`kfo_result`, `kfo_result_line`, `kfo_result_line_company` (DBML v5.26) в тыс. руб.: строки бланка редакции на +отчётную дату версии и служебную строку `GOODWILL`; по компании — `client_amount` (строки расходов 2120, 2210, 2220, +2330, 2350 приведены к `−|x|`), `vgo_amount` (Σ исключений компании по строке в рублях, до целых тысяч, половина от +нуля, со знаком строки), `amount = client_amount − vgo_amount`, `share_percent`; по строке — `consolidated_amount = +sum_amount − vgo_amount + goodwill_adjust`. Вход взаимных сумм SHALL быть агрегатами таблицы корректировок (§5 п. 2a); +применённая корректировка агрегата SHALL заменять его автосумму, а не прибавляться к ней. + +#### Scenario: Агрегат с корректировкой +- **WHEN** у агрегата правила 6 автосумма 0 (отклонение 400 %) и применена корректировка «Дт Б 1230 10 000 000, Кт А + 1520 10 000 000» +- **THEN** у Б по 1230 и у А по 1520 исключается по 10 000 тыс., автосумма агрегата не добавляется; у агрегата в + допуске с корректировкой автосумма тоже не добавляется + +#### Scenario: Меньший итог сторон корректировки +- **WHEN** у корректировки агрегата Дт = 20 000 000 (1230) + 5 000 000 (1170), Кт = 22 500 000 (1520) +- **THEN** каждая сторона исключает 22 500 000: компания 1 — 20 000 000 из 1230 и 2 500 000 из 1170, компания 2 — + 22 500 000 из 1520 + +#### Scenario: Каскад правила 2 +- **WHEN** агрегат правила 2 (`min_or_full`) без стороны 2 даёт 15 000 000, а у компании 2 строки 1310 = 10 000 тыс., + 1350 = 3 000 тыс. +- **THEN** у компании 1 исключается 15 000 тыс. из 1170, у компании 2 — 10 000 из 1310, 3 000 из 1350, 2 000 из 1370 + +#### Scenario: Гудвилл учтён +- **WHEN** у пары ГО → ДО таблицы гудвилла `use_goodwill` (ФВ 50 000 000, P 32 000 000, G 18 000 000) и + `goodwill_enabled = true` +- **THEN** у ГО из 1170 исключается 50 000 тыс., у ДО из 1310 — 32 000 тыс., строка `GOODWILL` = 18 000 тыс. и входит в + 1100 и 1600, агрегат правила 2 этой пары не применяется, `bank_amount` 1600 равен `bank_amount` 1700 + +#### Scenario: Гудвилл не учтён +- **WHEN** у той же пары `G ≤ 0` или `goodwill_enabled = false` +- **THEN** применяется агрегат правила 2 как обычно, строка `GOODWILL` = 0 + +#### Scenario: Расходы с разным знаком в формах +- **WHEN** у компании А строка 2120 хранится как −120 000, у компании Б как +60 000, и 10 000 тыс. исключаются из 2110 А + и 2120 Б +- **THEN** `client_amount` 2120 Б = −60 000, `vgo_amount` = −10 000, `amount` = −50 000; Σ `vgo_amount` строки 2100 = 0 + +#### Scenario: Нет формы у компании +- **WHEN** у компании периметра нет подтверждённой формы ОФР, привязанной к версии +- **THEN** КФО считается, значения компании по строкам ОФР = 0, в `kfo_result.warnings` — `FORM_MISSING` с компанией и + `statement_type = income` + +### Requirement: Автосумма агрегата одной функцией +Автосумма агрегата SHALL считаться одной функцией домена для таблицы корректировок (`eliminated_amount`) и для КФО, по +`elimination_rule.elimination_mode`: `min` — меньшая сторона в допуске, иначе 0; `min_or_full` — меньшая сторона при +обеих > 0, `amount_1` при пустой стороне 2, 0 при пустой стороне 1, без порога (`exceeds_threshold = false`); `manual` — 0. + +#### Scenario: Правило 2 в таблице корректировок +- **WHEN** у агрегата правила 2 сторона 2 пуста, `amount_1` = 15 000 000 +- **THEN** таблица корректировок отдаёт `status = ONE_SIDED`, `eliminated_amount = "15000000.00"`, `exceeds_threshold = + false`, и КФО исключает ту же сумму + +### Requirement: Чтение результата КФО +`GET /api/v1/consolidations/versions/{version_id}/kfo` (право `KFO_VIEW`) SHALL отдавать `KfoResultOut` по +`dev/api-contract-kfo.openapi.yaml`, деньги — строками с двумя знаками в тыс. руб., `units = "thousand_rub"`. Пока +версия не заморожена, ответ SHALL отражать текущие данные версии: устаревший результат пересчитывается и сохраняется +до ответа. У замороженной версии (`kfo_result.is_frozen`) — сохранённый результат без пересчёта, с сохранёнными +`warnings`. + +#### Scenario: Изменение после расчёта +- **WHEN** после `computed_at` аналитик сохранил корректировку агрегата +- **THEN** следующий `GET …/kfo` возвращает результат с учётом корректировки и новым `computed_at` + +#### Scenario: Замороженная версия +- **WHEN** версия в статусе «Верифицирован» и `is_frozen = true` +- **THEN** ответ — сохранённый результат, `editable = false`, `read_only_reason = "VERSION_NOT_EDITABLE"`, пересчёта нет + +### Requirement: Ручные поля КФО +`PUT /api/v1/consolidations/versions/{version_id}/kfo/annotations` (право `CONSOLIDATION_EDIT`, блокировка версии) +SHALL сохранять одной транзакцией комментарии строк (≤ 2000), `threshold_justification` и +`methodology_deviations` (≤ 4000): отсутствующее поле не меняется, `null` очищает; комментарии и обоснование +переживают пересчёт. Изменение SHALL писать одно событие `audit_event(KFO_EDIT)` раздела `KFO` с `before` / `after`. + +#### Scenario: Неизвестная строка +- **WHEN** в `comments[]` строка, которой нет в бланке версии +- **THEN** 422 `UNKNOWN_LINE` с `detail.statement_type`, `detail.line_code`, ничего не сохраняется + +#### Scenario: Сохранение обоснования +- **WHEN** у версии превышение порога ББ и аналитик с блокировкой сохраняет `threshold_justification` +- **THEN** ответ — `KfoResultOut` с этим обоснованием, в журнале одно событие `KFO_EDIT` diff --git a/openspec/changes/kfo-calculation-contract/tasks.md b/openspec/changes/kfo-calculation-contract/tasks.md new file mode 100644 index 0000000..66d5257 --- /dev/null +++ b/openspec/changes/kfo-calculation-contract/tasks.md @@ -0,0 +1,11 @@ +# Tasks: kfo-calculation-contract (docs #206) + +План — `plans/2026-11-06-kfo-contract.md`. + +- [x] 1. `context/05-domain-rules.md` §8 «КФО» (сквозной пример), §8 «Корректировка», §5 п. 1; `07-open-questions.md` OQ-180…193 +- [x] 2. `model/data-model-v5.26.dbml`, `model/entities-v5.26.md`, ссылки на текущую версию, диаграммы +- [x] 3. `dev/api-contract-kfo.md` + `.openapi.yaml`; `api-contract-corrections.*` — агрегат `min_or_full`; `frontend-api-guide.md` §5.7.3 +- [x] 4. Spec delta, `make check`, финальное ревью, PR, вложения в AURORA-1546 / AURORA-1547 +- [ ] 5. Документы контекста: `03-architecture` (без изменений — архитектура та же), `07-open-questions` (п. 1), `09-delivery-plan` (Срез 6 — после merge кода 1546/1547) + +Дочерние change'и — в `arm-core` (AURORA-1546, 1547, 1417); заводят исполнители. diff --git a/openspec/config.yaml b/openspec/config.yaml index 19f40d3..a1e991d 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,12 +1,12 @@ schema: spec-driven context: | - Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.24), правила домена, контракты между сервисами + Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.26), правила домена, контракты между сервисами (docs/contracts/*.schema.json), макет UI и экспорт ТЗ системы АРМ КА (ДКБ, Россельхозбанк). Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + проверки качества (make check) + изменения документов контекста. Код сервисов живёт в соседних репозиториях (../arm-core, ../tabular-parser, ../scan-gateway), у каждого свой openspec/ и свой change; сквозной change ссылается на них. - Источники истины по приоритету: docs/model/data-model-v5.24.dbml → docs/model/entities-v5.24.md → + Источники истины по приоритету: docs/model/data-model-v5.26.dbml → docs/model/entities-v5.26.md → docs/context/05-domain-rules.md → docs/source/tz (черновик, имена v4). Открытые вопросы — docs/context/07-open-questions.md; план срезов — docs/context/09-delivery-plan.md; соглашения шаблона команды — docs/context/03-architecture.md. Язык документов и коммитов — русский, идентификаторы — английские, как в DBML. docs/source/** не редактируется. diff --git a/repo-templates/arm-core.CLAUDE.md b/repo-templates/arm-core.CLAUDE.md index fb3db94..ae4f3cd 100644 --- a/repo-templates/arm-core.CLAUDE.md +++ b/repo-templates/arm-core.CLAUDE.md @@ -16,7 +16,7 @@ - NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`. ## Правила репозитория -- ORM 1:1 с `../docs/model/data-model-v5.24.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. +- ORM 1:1 с `../docs/model/data-model-v5.26.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. - Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring. - Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц. - Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR. diff --git a/scripts/kfo_contract/gen_kfo_openapi.py b/scripts/kfo_contract/gen_kfo_openapi.py new file mode 100644 index 0000000..e874b24 --- /dev/null +++ b/scripts/kfo_contract/gen_kfo_openapi.py @@ -0,0 +1,285 @@ +"""Генерирует dev/api-contract-kfo.openapi.yaml: пути, схемы и examples из kfo_example.py (docs #206). + +uv run --with pyyaml --with jsonschema --with openapi-spec-validator python -I gen_kfo_openapi.py +""" +import copy +import json +import sys +from decimal import Decimal as D +from pathlib import Path + +import yaml + +sys.path.insert(0, str(Path(__file__).parent)) +import kfo_example as ex # noqa: E402 + +MONEY = {"type": "string", "pattern": r"^-?\d{1,16}\.\d{2}$", "examples": ["588500.00"]} +MONEY_N = {"type": ["string", "null"], "pattern": r"^-?\d{1,16}\.\d{2}$"} +PCT_N = {"type": ["string", "null"], "pattern": r"^-?\d+\.\d{2}$"} # процент к базе не ограничен (база бывает мала) +SHARE_N = {"type": ["string", "null"], "pattern": r"^-?\d{1,3}\.\d{2}$"} +STYPE = {"type": "string", "enum": ["balance", "income"]} +ROR = {"type": ["string", "null"], "enum": ["VERSION_NOT_EDITABLE", "LOCK_REQUIRED", "LOCKED_BY_OTHER", None]} +WARN = ["FORM_MISSING", "FORM_AMBIGUOUS", "FORM_UNITS_UNKNOWN", "LINE_OUTSIDE_BLANK", "UNALLOCATED_ROWS", + "INCOME_RULES_MISSING", "GOODWILL_NOT_APPLIED", "GOODWILL_ROW_REASSIGNED", + "CORRECTION_SUPERSEDED_BY_GOODWILL"] + + +def obj(props, required=None, desc=None): + o = {"type": "object", "additionalProperties": False, "properties": props, + "required": list(props) if required is None else required} + if desc: + o["description"] = desc + return o + + +def ref(n): + return {"$ref": f"#/components/schemas/{n}"} + + +SCHEMAS = { + "KfoLineCompanyOut": obj({ + "legal_entity_id": {"type": "string", "format": "uuid"}, + "client_amount": {**MONEY, "description": "«По данным клиента», тыс. руб.; строки расходов приведены к −|x| (§8 «КФО» п. 2)"}, + "vgo_amount": {**MONEY, "description": "«В т. ч. взаимные», тыс. руб. до целых, со знаком строки (п. 6)"}, + "cleaned_amount": {**MONEY, "description": "«Очищенные показатели» = client_amount − vgo_amount"}, + "share_percent": {**SHARE_N, "description": "«Доля в показателе по группе, %» от bank_amount строки, ROUND_HALF_UP; null при нулевом итоге или |доля| ≥ 1000 %"}, + }), + "KfoLineOut": obj({ + "line_code": {"type": "string", "description": "код строки бланка редакции на отчётную дату или GOODWILL"}, + "line_name": {"type": "string"}, + "ordinal": {"type": "integer", "description": "порядок строки в бланке"}, + "is_total": {"type": "boolean", "description": "итоговая строка бланка (1100, 1600, 2100 …)"}, + "is_threshold_base": {"type": "boolean", "description": "строка — база порога (1300, 1600, 2110 или 2200), подсветка при превышении"}, + "companies": {"type": "array", "items": ref("KfoLineCompanyOut"), + "description": "по компаниям в порядке KfoResultOut.companies; у GOODWILL — пусто"}, + "sum_amount": {**MONEY, "description": "Σ client_amount"}, + "vgo_amount": {**MONEY, "description": "Σ vgo_amount = eliminated_auto + eliminated_manual"}, + "eliminated_auto": MONEY, + "eliminated_manual": MONEY, + "goodwill_adjust": {**MONEY, "description": "Σ G — у GOODWILL и итогов 1100, 1600"}, + "bank_amount": {**MONEY, "description": "«КФО, подготовленная банком» = sum_amount − vgo_amount + goodwill_adjust"}, + "deviation_amount": {**MONEY_N, "description": "«Расхождения» (§8 «КФО» п. 9); у итогов — null"}, + "comment": {"type": ["string", "null"], "maxLength": 2000}, + }), + "KfoStatementOut": obj({"lines": {"type": "array", "items": ref("KfoLineOut")}}), + "KfoThresholdOut": obj({ + "deviation_total": MONEY, + "base_kind": {"type": "string", "enum": ["NET_ASSETS", "BALANCE_TOTAL", "SALES_PROFIT", "REVENUE"]}, + "base_amount": MONEY, + "limit_percent": {"type": "string", "enum": ["10.00", "1.00"]}, + "limit_amount": MONEY, + "deviation_percent": {**PCT_N, "description": "deviation_total / base_amount × 100; null при нулевой базе"}, + "exceeded": {"type": "boolean", "description": "deviation_total > limit_amount (равно — норма)"}, + }), + "KfoSummaryOut": obj({"balance": ref("KfoThresholdOut"), "income": ref("KfoThresholdOut")}), + "KfoCompanyOut": obj({"legal_entity_id": {"type": "string", "format": "uuid"}, "name": {"type": "string"}, + "inn": {"type": "string"}}), + "KfoCorrectionsOut": obj({"count": {"type": "integer", "minimum": 0}, "amount": MONEY}, + desc="агрегаты с применённой корректировкой и Σ их исключений, тыс. руб."), + "KfoRequisitesOut": obj({ + "company_group_name": {"type": "string"}, + "related_group_name": {"type": ["string", "null"]}, + "reporting_date": {"type": "string", "format": "date"}, + "version_no": {"type": "integer"}, + "version_status": {"type": "string", "description": "consolidation_version.status (enum consolidation_status DBML)", + "enum": ["INITIATED", "DOCUMENTS_PROCESSING", "DATA_CORRECTION", "CONSOLIDATION_RF", "ADJUSTMENT", + "VERIFICATION_RF", "CONSOLIDATION_GO", "VERIFICATION_GO", "VERIFIED", "CANCELLED", + "REJECTED", "ARCHIVED"]}, + "scenario": {"type": "string", "enum": ["CREATE", "CONFIRM"]}, + "approach": {"type": "string", "enum": ["BASIC", "EXTENDED"]}, + "methodology_deviations": {"type": ["string", "null"], "maxLength": 4000}, + "uob_decision_no": {"type": ["string", "null"]}, + "uob_decision_date": {"type": ["string", "null"], "format": "date"}, + "corrections_balance": ref("KfoCorrectionsOut"), + "corrections_income": ref("KfoCorrectionsOut"), + }), + "KfoGoodwillOut": obj({"enabled": {"type": "boolean"}, "total": MONEY}), + "KfoWarningOut": obj({ + "code": {"type": "string", "enum": WARN}, + "message": {"type": "string"}, + "legal_entity_id": {"type": ["string", "null"], "format": "uuid"}, + "statement_type": {"type": ["string", "null"], "enum": ["balance", "income", None]}, + "count": {"type": ["integer", "null"]}, + }, required=["code", "message"]), + "KfoResultOut": obj({ + "version_id": {"type": "string", "format": "uuid"}, + "computed_at": {"type": "string", "format": "date-time"}, + "is_frozen": {"type": "boolean"}, + "editable": {"type": "boolean"}, + "read_only_reason": ROR, + "units": {"type": "string", "const": "thousand_rub"}, + "companies": {"type": "array", "items": ref("KfoCompanyOut")}, + "requisites": ref("KfoRequisitesOut"), + "summary": ref("KfoSummaryOut"), + "justification_required": {"type": "boolean"}, + "threshold_justification": {"type": ["string", "null"], "maxLength": 4000}, + "goodwill": ref("KfoGoodwillOut"), + "balance": ref("KfoStatementOut"), + "income": ref("KfoStatementOut"), + "warnings": {"type": "array", "items": ref("KfoWarningOut")}, + }), + "KfoCommentIn": obj({"statement_type": STYPE, "line_code": {"type": "string"}, + "comment": {"type": ["string", "null"], "maxLength": 2000}}), + "KfoAnnotationsIn": obj({ + "comments": {"type": "array", "items": ref("KfoCommentIn")}, + "threshold_justification": {"type": ["string", "null"], "maxLength": 4000}, + "methodology_deviations": {"type": ["string", "null"], "maxLength": 4000}, + }, required=[], desc="Отсутствующее поле не меняется, null — очищает"), + "ErrorDetail": obj({"code": {"type": "string"}, "message": {"type": "string"}, + "line_code": {"type": ["string", "null"]}, + "statement_type": {"type": ["string", "null"]}}, required=["code", "message"]), + "ErrorOut": obj({"detail": ref("ErrorDetail")}), +} + +IDS = {"A": "11111111-1111-4111-8111-111111111111", "B": "22222222-2222-4222-8222-222222222222", + "C": "33333333-3333-4333-8333-333333333333"} +VERSION = "99999999-9999-4999-8999-999999999999" +NAMES_BB = {"1170": "Финансовые вложения", "GOODWILL": "Гудвилл", "1100": "Итого по разделу I", + "1230": "Дебиторская задолженность", "1200": "Итого по разделу II", "1600": "БАЛАНС", + "1310": "Уставный капитал", "1350": "Добавочный капитал (без переоценки)", + "1370": "Нераспределенная прибыль (непокрытый убыток)", "1300": "Итого по разделу III", + "1410": "Заемные средства", "1400": "Итого по разделу IV", "1520": "Кредиторская задолженность", + "1500": "Итого по разделу V", "1700": "БАЛАНС"} +NAMES_OFR = {"2110": "Выручка", "2120": "Себестоимость продаж", "2100": "Валовая прибыль (убыток)", + "2210": "Коммерческие расходы", "2220": "Управленческие расходы", "2200": "Прибыль (убыток) от продаж"} + + +def m(x): + return None if x is None else f"{D(x):.2f}".replace("-0.00", "0.00") + + +def lines(rows, order, names, base_code, exceeded, comments=None): + out = [] + for i, code in enumerate(order, 1): + r = rows[code] + comps = [] + if r["companies"]: + for c in ex.COMPANIES: + x = r["companies"][c] + comps.append({"legal_entity_id": IDS[c], "client_amount": m(x["client"]), "vgo_amount": m(x["vgo"]), + "cleaned_amount": m(x["cleaned"]), "share_percent": m(x["share"])}) + out.append({"line_code": code, "line_name": names[code], "ordinal": i * 10, + "is_total": code in ex.TOTALS, "is_threshold_base": exceeded and code == base_code, + "companies": comps, "sum_amount": m(r["sum"]), "vgo_amount": m(r["vgo"]), + "eliminated_auto": m(r["auto"]), "eliminated_manual": m(r["manual"]), + "goodwill_adjust": m(r["goodwill_adjust"]), "bank_amount": m(r["bank"]), + "deviation_amount": m(r["dev"]), "comment": (comments or {}).get(code)}) + return out + + +def result(enabled, frozen=False): + bb, ofr, summ, g = ex.compute(enabled) + base_code = {"NET_ASSETS": "1300", "BALANCE_TOTAL": "1600", "SALES_PROFIT": "2200", "REVENUE": "2110"} + + def thr(s): + return {"deviation_total": m(s["deviation_total"]), "base_kind": s["base_kind"], + "base_amount": m(s["base_amount"]), "limit_percent": m(s["limit_percent"]), + "limit_amount": m(s["limit_amount"]), "deviation_percent": m(s["deviation_percent"]), + "exceeded": s["exceeded"]} + comments = {"1520": "Расхождение Бета → Альфа по 62/60: акт сверки на 01.10 не получен"} if frozen else None + warnings = [{"code": "FORM_MISSING", "message": "Нет подтверждённой формы ОФР у ООО «Гамма»", + "legal_entity_id": IDS["C"], "statement_type": "income", "count": None}] + return { + "version_id": VERSION, "computed_at": "2026-11-10T09:15:00Z", "is_frozen": frozen, + "editable": not frozen, "read_only_reason": "VERSION_NOT_EDITABLE" if frozen else None, + "units": "thousand_rub", + "companies": [{"legal_entity_id": IDS[c], "name": ex.NAMES[c], "inn": ex.INN[c]} for c in ex.COMPANIES], + "requisites": {"company_group_name": "ГК «Альфа»", "related_group_name": "ГСК «Альфа»", + "reporting_date": "2025-10-01", "version_no": 1, + "version_status": "VERIFIED" if frozen else "CONSOLIDATION_RF", + "scenario": "CREATE", "approach": "BASIC", + "methodology_deviations": "Отклонений от Методики нет" if frozen else None, + "uob_decision_no": "УОБ-118" if frozen else None, + "uob_decision_date": "2025-11-20" if frozen else None, + "corrections_balance": {"count": 2, "amount": "65000.00"}, + "corrections_income": {"count": 0, "amount": "0.00"}}, + "summary": {"balance": thr(summ["balance"]), "income": thr(summ["income"])}, + "justification_required": summ["balance"]["exceeded"] or summ["income"]["exceeded"], + "threshold_justification": ("Расхождение по 1520 — Бета → Альфа, сверка в работе; риск принят" + if frozen else None), + "goodwill": {"enabled": enabled, "total": m(g / ex.K) if enabled else "0.00"}, + "balance": {"lines": lines(bb, ex.BB_LINES, NAMES_BB, base_code[summ["balance"]["base_kind"]], + summ["balance"]["exceeded"], comments)}, + "income": {"lines": lines(ofr, ex.OFR_LINES, NAMES_OFR, base_code[summ["income"]["base_kind"]], + summ["income"]["exceeded"])}, + "warnings": warnings, + } + + +def err(desc, code, extra=None): + e = {"description": desc, "content": {"application/json": {"schema": ref("ErrorOut")}}} + return e + + +PATH_PARAM = [{"name": "version_id", "in": "path", "required": True, "schema": {"type": "string", "format": "uuid"}}] + + +def build(): + ex1, ex2, ex3 = result(True), result(False), result(True, frozen=True) + doc = { + "openapi": "3.1.0", + "info": {"title": "АРМ КА — результат КФО (AURORA-1546/1547)", "version": "0.1", + "description": "Контракт до реализации (docs #206). Правила — context/05-domain-rules.md §8 «КФО»; " + "текст — dev/api-contract-kfo.md. Деньги — строки с двумя знаками в тыс. руб. (units)."}, + "paths": { + "/api/v1/consolidations/versions/{version_id}/kfo": {"get": { + "tags": ["consolidations"], "summary": "Результат КФО версии", + "description": "Право KFO_VIEW. Пока версия не заморожена — результат по текущим данным: устаревший " + "пересчитывается и сохраняется до ответа. Замороженный — только чтение сохранённого.", + "operationId": "kfo_get", "security": [{"bearerAuth": []}], "parameters": PATH_PARAM, + "responses": { + "200": {"description": "Результат КФО", "content": {"application/json": { + "schema": ref("KfoResultOut"), + "examples": { + "goodwill_exceeded": {"summary": "Гудвилл включён, превышение порога ББ (сквозной пример §8)", "value": ex1}, + "goodwill_disabled": {"summary": "Та же версия, учёт гудвилла выключен", "value": ex2}, + "frozen": {"summary": "Версия «Верифицирован»: только чтение, обоснование и комментарий заполнены", "value": ex3}, + }}}}, + "403": err("Нет права KFO_VIEW", "FORBIDDEN"), + "404": err("Версия не найдена или вне видимости — NOT_FOUND", "NOT_FOUND"), + }}}, + "/api/v1/consolidations/versions/{version_id}/kfo/annotations": {"put": { + "tags": ["consolidations"], "summary": "Ручные поля КФО", + "description": "Право CONSOLIDATION_EDIT, блокировка версии. Комментарии строк, обоснование порога, " + "причины отклонений от Методики — одной транзакцией; отсутствующее поле не меняется, " + "null очищает. Аудит KFO_EDIT. Ответ — KfoResultOut целиком.", + "operationId": "kfo_annotations_put", "security": [{"bearerAuth": []}], "parameters": PATH_PARAM, + "requestBody": {"required": True, "content": {"application/json": { + "schema": ref("KfoAnnotationsIn"), + "examples": {"justify": {"summary": "Обоснование и комментарий строки", "value": { + "comments": [{"statement_type": "balance", "line_code": "1520", + "comment": "Расхождение Бета → Альфа по 62/60: акт сверки на 01.10 не получен"}], + "threshold_justification": "Расхождение по 1520 — Бета → Альфа, сверка в работе; риск принят"}}}}}}, + "responses": { + "200": {"description": "Результат КФО после сохранения", + "content": {"application/json": {"schema": ref("KfoResultOut")}}}, + "403": err("Нет права CONSOLIDATION_EDIT", "FORBIDDEN"), + "404": err("NOT_FOUND", "NOT_FOUND"), + "409": err("VERSION_NOT_EDITABLE | LOCK_REQUIRED | LOCKED_BY_OTHER", "VERSION_NOT_EDITABLE"), + "422": {"description": "UNKNOWN_LINE (detail.statement_type, detail.line_code) или массив ошибок " + "валидации тела (длины 2000/4000)", + "content": {"application/json": {"schema": {"oneOf": [ref("ErrorOut"), {"type": "object"}]}}}}, + }}}, + }, + "components": {"schemas": SCHEMAS, + "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}}}, + } + return doc, (ex1, ex2, ex3) + + +if __name__ == "__main__": + doc, examples = build() + import jsonschema + from openapi_spec_validator import validate + validate(copy.deepcopy(doc)) + root = {"$defs": SCHEMAS} + resolver_schema = json.loads(json.dumps(SCHEMAS).replace("#/components/schemas/", "#/$defs/")) + for name, e in zip(("goodwill_exceeded", "goodwill_disabled", "frozen"), examples): + jsonschema.validate(e, {"$defs": resolver_schema, **resolver_schema["KfoResultOut"]}, + cls=jsonschema.Draft202012Validator) + print("example ok:", name) + Path(sys.argv[1]).write_text( + "# Сгенерировано scripts/kfo_contract/gen_kfo_openapi.py из сквозного примера §8 «КФО» (docs #206).\n" + "# Руками не править: правка генератора (и kfo_example.py) → перегенерация; текст — dev/api-contract-kfo.md.\n" + + yaml.safe_dump(doc, allow_unicode=True, sort_keys=False, width=120), encoding="utf-8") + print("openapi ok") diff --git a/scripts/kfo_contract/kfo_example.py b/scripts/kfo_contract/kfo_example.py new file mode 100644 index 0000000..d4b10ba --- /dev/null +++ b/scripts/kfo_contract/kfo_example.py @@ -0,0 +1,315 @@ +"""Сквозной пример §8 «КФО» (docs #206): мини-движок по design.md §4.1, числа для §8 и examples контракта. + +Запуск: python3 -I kfo_example.py [--json out.json] +""" +import json +import sys +from decimal import ROUND_HALF_UP, Decimal as D + +K = D("1000") +EXPENSE = {"2120", "2210", "2220", "2330", "2350"} +COMPANIES = ["A", "B", "C"] +NAMES = {"A": "ООО «Альфа»", "B": "ООО «Бета»", "C": "ООО «Гамма»"} +INN = {"A": "7701000001", "B": "7701000002", "C": "7701000003"} + +# Формы, тыс. руб. (как в форме; знак расходов у «Беты» — «без знака») +BB = { + "A": {"1170": 70000, "1230": 520000, "1100": 70000, "1200": 520000, "1600": 590000, + "1310": 100000, "1350": 0, "1370": 280000, "1300": 380000, "1410": 50000, "1400": 50000, + "1520": 160000, "1500": 160000, "1700": 590000}, + "B": {"1170": 0, "1230": 130000, "1100": 0, "1200": 130000, "1600": 130000, + "1310": 40000, "1350": 0, "1370": -20000, "1300": 20000, "1410": 0, "1400": 0, + "1520": 110000, "1500": 110000, "1700": 130000}, + "C": {"1170": 0, "1230": 48000, "1100": 0, "1200": 48000, "1600": 48000, + "1310": 10000, "1350": 3000, "1370": 5000, "1300": 18000, "1410": 0, "1400": 0, + "1520": 30000, "1500": 30000, "1700": 48000}, +} +OFR = { + "A": {"2110": 200000, "2120": -120000, "2100": 80000, "2210": -20000, "2220": -10000, "2200": 50000}, + "B": {"2110": 80000, "2120": 60000, "2100": 20000, "2210": -5000, "2220": 0, "2200": 15000}, + # «Гамма» — формы ОФР нет: FORM_MISSING +} +BB_LINES = ["1170", "GOODWILL", "1100", "1230", "1200", "1600", "1310", "1350", "1370", "1300", + "1410", "1400", "1520", "1500", "1700"] +OFR_LINES = ["2110", "2120", "2100", "2210", "2220", "2200"] +TOTALS = { # итог ← слагаемые (с учётом знака, значения уже со знаком) + "1100": ["1170", "GOODWILL"], "1200": ["1230"], "1600": ["1100", "1200"], + "1300": ["1310", "1350", "1370"], "1400": ["1410"], "1500": ["1520"], "1700": ["1300", "1400", "1500"], + "2100": ["2110", "2120"], "2200": ["2100", "2210", "2220"], +} + +# Агрегаты таблицы корректировок (рубли). rule: (mode, article_1, article_2) +RULES = {2: ("min_or_full", "1170", "1310/1350/1370"), 6: ("min", "1230", "1520"), + "ofr1": ("full_side1_and_side2", "2110", "2120")} +AGGS = [ + # (rule, c1, c2, amount_1, amount_2, correction) + (6, "A", "B", D("102000000.00"), D("100000400.50"), None), + (6, "B", "A", D("10000000.00"), D("50000000.00"), + {"debit": [("1230", D("10000000.00"))], "credit": [("1520", D("10000000.00"))]}), + (6, "A", "C", D("30000000.00"), D("29100000.00"), + {"debit": [("1230", D("20000000.00")), ("1170", D("5000000.00"))], "credit": [("1520", D("22500000.00"))]}), + (2, "A", "B", D("50000000.00"), D("40000000.00"), None), + (2, "A", "C", D("15000000.00"), D("0.00"), None), + ("ofr1", "A", "B", D("10000000.00"), D("10000000.00"), None), # S без НДС (правила 1999) +] +GOODWILL_PAIRS = [ # ГО, ДО, ФВ, УК (руб.), доля %, отмечена + ("A", "B", D("50000000.00"), D("40000000.00"), D("80.00"), True), +] +THRESHOLD = D("5") + + +def r2(x): + return x.quantize(D("0.01"), ROUND_HALF_UP) + + +def k_int(rub): # рубли → целые тысячи, половина от нуля + return (rub / K).quantize(D("1"), ROUND_HALF_UP) + + +def split_vgo(auto_rub, manual_rub, sign): + """Ячейка компания × строка: vgo — округлённая Σ исключений; manual округляется так же; auto = vgo − manual.""" + vgo = sign * k_int(auto_rub + manual_rub) + man = sign * k_int(manual_rub) + return vgo, vgo - man, man + + +def auto_amount(mode, a1, a2): + """Автосумма агрегата по elimination_mode (без порога для min — порог проверяется отдельно).""" + if mode == "min": + return min(a1, a2) if a1 > 0 and a2 > 0 else D(0) + if mode == "min_or_full": + if a1 <= 0: + return D(0) + return min(a1, a2) if a2 > 0 else a1 + return D(0) + + +def agg_deviation(mode, a1, a2): + """Расхождение агрегата для порога: только режим min, в том числе односторонний.""" + return abs(a1 - a2) if mode == "min" else D(0) + + +def share(cleaned, bank): + if not bank: + return None + x = r2(cleaned / bank * 100) + return None if abs(x) >= 1000 else x + + +def total_client(form_c, line, parts): + """Итог «по данным клиента»: из формы, а если строки итога в форме нет — Σ слагаемых.""" + if line in form_c: + return D(form_c[line]) + return sum((D(form_c.get(p_, 0)) for p_ in parts), D(0)) + + +def client(form, c, line): + v = D(form.get(c, {}).get(line, 0)) + return -abs(v) if line in EXPENSE else v + + +def compute(goodwill_enabled=True): + excl = {} # (c, line) -> рубли + auto, manual = {}, {} + dev = {} + log = [] + + def add(c, line, amt, kind): + excl[(c, line)] = excl.get((c, line), D(0)) + amt + (auto if kind == "auto" else manual)[(c, line)] = (auto if kind == "auto" else manual).get((c, line), D(0)) + amt + log.append((c, line, amt, kind)) + + def available(c, line): + return max(client(BB, c, line) * K - excl.get((c, line), D(0)), D(0)) + + def cascade(c, lines, amt, kind): + codes = lines.split("/") + for i, code in enumerate(codes): + take = amt if i == len(codes) - 1 else min(amt, available(c, code)) + if take > 0: + add(c, code, take, kind) + amt -= take + if amt <= 0: + break + + gw_applied = [] + g_total = D(0) + for go, do, fv, uk, d, marked in GOODWILL_PAIRS: + p = r2(uk * d / 100) + g = fv - p + if goodwill_enabled and marked and g > 0: + gw_applied.append((go, do)) + add(go, "1170", fv, "auto") + add(do, "1310", p, "auto") + g_total += g + + order = sorted(AGGS, key=lambda a: (str(a[0]), INN[a[1]], INN[a[2]])) + for rule, c1, c2, a1, a2, corr in order: + mode, art1, art2 = RULES[rule] + dv = agg_deviation(mode, a1, a2) + if dv: + line_dev = (c1, art1) if a1 > a2 else (c2, art2) + dev[line_dev] = dev.get(line_dev, D(0)) + dv + if rule == 2 and (c1, c2) in gw_applied: + continue # гудвилл-пара подавляет агрегат правила 2 + if corr: + dt = sum((x for _, x in corr["debit"]), D(0)) + kt = sum((x for _, x in corr["credit"]), D(0)) + lim = min(dt, kt) if dt > 0 and kt > 0 else None + for side, c in (("debit", c1), ("credit", c2)): + rest = lim + for line, x in corr[side]: + take = x if rest is None else min(x, rest) + if take > 0: + add(c, line, take, "manual") + if rest is not None: + rest -= take + continue + if mode == "min": + pct = abs(a1 - a2) / min(a1, a2) * 100 if a1 > 0 and a2 > 0 else None + s = auto_amount(mode, a1, a2) if pct is not None and pct <= THRESHOLD else D(0) + if s: + add(c1, art1, s, "auto"); add(c2, art2, s, "auto") + elif mode == "min_or_full": + s = auto_amount(mode, a1, a2) + if s: + add(c1, art1, s, "auto"); cascade(c2, art2, s, "auto") + elif mode == "full_side1_and_side2": + add(c1, art1, a1, "auto"); add(c2, art2, a2, "auto") + + def build(form, lines, stype): + out = {} + for line in lines: + if line == "GOODWILL": + ga = k_int(g_total) + out[line] = {"companies": None, "sum": D(0), "vgo": D(0), "auto": D(0), "manual": D(0), + "goodwill_adjust": ga, "bank": ga, "dev": D(0)} + continue + comps = {} + if line in TOTALS: + for c in COMPANIES: + cl = total_client(form[c], line, TOTALS[line]) if c in form else D(0) + vg = sum((out[s]["companies"][c]["vgo"] for s in TOTALS[line] if out[s]["companies"]), D(0)) + comps[c] = {"client": cl, "vgo": vg, "cleaned": cl - vg} + ga = sum((out[s]["goodwill_adjust"] for s in TOTALS[line]), D(0)) + au = sum((out[s]["auto"] for s in TOTALS[line]), D(0)) + ma = sum((out[s]["manual"] for s in TOTALS[line]), D(0)) + dv = None # у итоговых строк расхождений нет + else: + ga = D(0) + au = ma = D(0) + for c in COMPANIES: + cl = client(form, c, line) if c in form else D(0) + sign = -1 if line in EXPENSE else 1 + vg, a_, m_ = split_vgo(auto.get((c, line), D(0)), manual.get((c, line), D(0)), sign) + comps[c] = {"client": cl, "vgo": vg, "cleaned": cl - vg} + au += a_ + ma += m_ + dv = k_int(sum((v for (c, l), v in dev.items() if l == line), D(0))) + dv = D(0) if stype == "income" else dv + s = sum((x["client"] for x in comps.values()), D(0)) + vgo = sum((x["vgo"] for x in comps.values()), D(0)) + bank = s - vgo + ga + for x in comps.values(): + x["share"] = share(x["cleaned"], bank) + out[line] = {"companies": comps, "sum": s, "vgo": vgo, "auto": au, "manual": ma, + "goodwill_adjust": ga, "bank": bank, "dev": dv} + return out + + bb = build(BB, BB_LINES, "balance") + ofr = build(OFR, OFR_LINES, "income") + # Расхождения: только строки бланка, не итоги + dev_bb = sum((v["dev"] for k_, v in bb.items() if k_ not in TOTALS), D(0)) + na, vb = bb["1300"]["bank"], bb["1600"]["bank"] + if na >= vb * D("0.10"): + base_bb, lim_pct_bb, base_kind_bb = na, D("10"), "NET_ASSETS" + else: + base_bb, lim_pct_bb, base_kind_bb = vb, D("1"), "BALANCE_TOTAL" + lim_bb = r2(base_bb * lim_pct_bb / 100) + rev, sp = ofr["2110"]["bank"], ofr["2200"]["bank"] + dev_ofr = D(0) + if rev != 0 and sp >= 0 and sp / rev >= D("0.10"): + base_ofr, lim_pct_ofr, base_kind_ofr = sp, D("10"), "SALES_PROFIT" + else: + base_ofr, lim_pct_ofr, base_kind_ofr = rev, D("1"), "REVENUE" + lim_ofr = r2(base_ofr * lim_pct_ofr / 100) + summary = { + "balance": {"deviation_total": dev_bb, "base_kind": base_kind_bb, "base_amount": base_bb, + "limit_percent": lim_pct_bb, "limit_amount": lim_bb, + "deviation_percent": r2(dev_bb / base_bb * 100) if base_bb else None, + "exceeded": dev_bb > lim_bb}, + "income": {"deviation_total": dev_ofr, "base_kind": base_kind_ofr, "base_amount": base_ofr, + "limit_percent": lim_pct_ofr, "limit_amount": lim_ofr, + "deviation_percent": r2(dev_ofr / base_ofr * 100) if base_ofr else None, + "exceeded": dev_ofr > lim_ofr}, + } + + # Проверки (Review Focus) + assert bb["1600"]["bank"] == bb["1700"]["bank"], (bb["1600"]["bank"], bb["1700"]["bank"]) + assert ofr["2100"]["vgo"] == 0, ofr["2100"]["vgo"] + assert ofr["2100"]["bank"] == ofr["2100"]["sum"] + # агрегат с корректировкой: B→A правило 6 — только 10 000 тыс., автосуммы нет + assert bb["1230"]["companies"]["B"]["vgo"] == D("10000") + # каскад правила 2 (A→C): Σ у «Гаммы» по 1310/1350/1370 = 15 000 = исключению из 1170 по этой паре + c_r2 = sum((amt for c, l, amt, _ in log if c == "C" and l in ("1310", "1350", "1370")), D(0)) + assert c_r2 == D("15000000.00"), c_r2 + assert k_int(excl[("C", "1370")]) == D("2000") + # меньший итог сторон корректировки A→C: 25 000 vs 22 500 → 22 500 с каждой стороны + a_from_ac = sum((amt for c, l, amt, kind in log if c == "A" and kind == "manual" and l in ("1230", "1170")), D(0)) + assert a_from_ac == D("22500000.00"), a_from_ac + if goodwill_enabled: + assert bb["GOODWILL"]["bank"] == D("18000") + assert bb["1100"]["goodwill_adjust"] == D("18000") and bb["1600"]["goodwill_adjust"] == D("18000") + else: + assert bb["GOODWILL"]["bank"] == 0 + return bb, ofr, summary, g_total + + +def show(title, rows, lines): + print(f"\n## {title}") + print("| Строка | " + " | ".join(f"{c}: клиент / взаимные / очищ. / доля" for c in COMPANIES) + + " | Σ клиент | взаимные | гудвилл | КФО банка | расхожд. |") + for line in lines: + r = rows[line] + cells = [] + for c in COMPANIES: + x = (r["companies"] or {}).get(c) + cells.append("—" if x is None else f"{x['client']} / {x['vgo']} / {x['cleaned']} / {x['share']}") + print(f"| {line} | " + " | ".join(cells) + + f" | {r['sum']} | {r['vgo']} | {r['goodwill_adjust']} | {r['bank']} | {r['dev']} |") + + +def self_test(): + """Проверки правил, которые сквозной пример не задевает (ревью ветки I-2, I-3, I-4, I-6).""" + # I-2: auto и manual — из одной округлённой ячейки: manual округляется, auto = vgo − manual + assert split_vgo(D("500.50"), D("500.50"), 1) == (D("1"), D("0"), D("1")) + assert split_vgo(D("0"), D("700.00"), -1) == (D("-1"), D("0"), D("-1")) + # I-3: автосумма min_or_full при отрицательной стороне 2 — как при пустой; при amount_1 ≤ 0 — 0 + assert auto_amount("min_or_full", D("100"), D("-5")) == D("100") + assert auto_amount("min_or_full", D("-1"), D("50")) == D("0") + # I-3: расхождение агрегата min — и у одностороннего (|a1 − a2|), у min_or_full — нет + assert agg_deviation("min", D("1000"), D("0")) == D("1000") + assert agg_deviation("min", D("100"), D("-50")) == D("150") + assert agg_deviation("min_or_full", D("1000"), D("0")) == D("0") + # I-4: доля по модулю ≥ 1000 % — null + assert share(D("280000"), D("1")) is None + assert share(D("400000"), D("568000")) == D("70.42") + # I-6: итога нет в форме (упрощённая) — client итога = Σ client слагаемых + assert total_client({"1170": D("10"), "1150": D("5")}, "1100", ["1170", "1150"]) == D("15") + assert total_client({"1100": D("20"), "1170": D("10")}, "1100", ["1170"]) == D("20") + + +if __name__ == "__main__": + self_test() + res = {} + for enabled in (True, False): + bb, ofr, summary, g = compute(enabled) + tag = "гудвилл включён" if enabled else "гудвилл выключен" + show(f"ББ, {tag}", bb, BB_LINES) + show(f"ОФР, {tag}", ofr, OFR_LINES) + print("\nСводка:", json.dumps(summary, default=str, ensure_ascii=False)) + res["enabled" if enabled else "disabled"] = {"balance": bb, "income": ofr, "summary": summary} + if "--json" in sys.argv: + with open(sys.argv[sys.argv.index("--json") + 1], "w") as f: + json.dump(res, f, default=str, ensure_ascii=False, indent=1) + print("\nOK: все проверки прошли")