DBML v5.23 (elimination_rules_version NOT NULL), §5 без каскада, пары ВГО по версии (#164, AURORA-1536)

Решения владельца 23.10.2026 по ревью arm-core #175:
- consolidation_version.elimination_rules_version — not null, существующим
  версиям номер проставляет миграция по дате создания;
- §5: каскад по остатку отменён (21.10), правила считаются отдельно до
  разнесения строк AURORA-1751; суммы сторон со знаком, при min <= 0 процент
  не считается; отрицательные строки в упаковку не идут (NEGATIVE_AMOUNT);
  группы сортируются по проценту расхождения; выдача — список троек версии
  и детализация тройки;
- OQ-173, OQ-174, OQ-175; страница аналитика; гайд фронта §5.7.2;
  ссылки на модель и диаграммы — v5.23.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-23 18:34:21 +04:00
parent e1c4523554
commit e70f589112
17 changed files with 1910 additions and 80 deletions

View File

@ -6,7 +6,7 @@ PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3)
# Пусто — проверяются все change'и и спеки; `make validate-openspec CHANGE=<id>` — один change
CHANGE ?=
# Текущая версия модели данных — по ней рисуются диаграммы (make diagrams)
DBML ?= model/data-model-v5.22.dbml
DBML ?= model/data-model-v5.23.dbml
.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-statements sync-statements statements-check check diagrams

View File

@ -1,7 +1,7 @@
# Элиминация ВГО: правила, расчёт, корректировки
Документ для аналитика, 19.10.2026 (RSHB/arm-ka-docs#132; сопоставление — по решениям OQ-07 и OQ-173, #136). Основа — модель данных
[`model/data-model-v5.19.dbml`](../model/data-model-v5.19.dbml) и правила предметной области
[`model/data-model-v5.23.dbml`](../model/data-model-v5.23.dbml) и правила предметной области
[`context/05-domain-rules.md`](../context/05-domain-rules.md) §3–§6, §8. Источник истины — они: при расхождении прав
DBML и `05-domain-rules.md`, а этот документ поправь следом. Где написано наше прочтение ТЗ, а не прямая цитата, это
помечено; такие места собраны в раздел 6 вопросами.
@ -90,8 +90,9 @@ DBML и `05-domain-rules.md`, а этот документ поправь сле
### 2.3. Версии правил
Правила меняются выпуском новой версии (`version_no`), старая не правится. Версия, по которой посчитана версия
консолидации, фиксируется в `consolidation_version.elimination_rules_version` при первом расчёте и замораживается при
`VERIFIED` — так расчёт верифицированной версии объясним и воспроизводим, даже если правила потом поменялись.
консолидации, фиксируется в `consolidation_version.elimination_rules_version` при создании версии (версия правил,
действующая на дату создания, OQ-174; с DBML v5.23 поле обязательно) и дальше не меняется — так расчёт объясним и
воспроизводим, даже если правила потом поменялись.
### 2.4. Стартовый набор — Прил. 3 (ББ, `measure = closing`)
@ -135,38 +136,36 @@ DBML и `05-domain-rules.md`, а этот документ поправь сле
компаний — и (A, B), и (B, A), поэтому зеркальные правила № 9–16 Прил. 3 отдельно не заводятся (№ 14 для (A, B) — это
№ 6 для (B, A)). У тройки два уровня (решения владельца 19.10.2026, OQ-07 и OQ-173):
**Сумма элиминации** — агрегат, идёт в КФО. Считается **каскадом по остатку**: правила пары (A, B) идут по порядку
`rule_no`, каждое берёт min из того, что осталось после предыдущих правил:
**Сумма элиминации** — агрегат, идёт в КФО. Каждое правило считается **отдельно** (каскад по остатку отменён решением
владельца 21.10.2026 по созвону с заказчиком 19.10.2026):
```
для правил по возрастанию rule_no:
ΣA = Σ остатка строк A с контрагентом B по счетам стороны 1 (с исключениями)
ΣB = Σ остатка строк B с контрагентом A по счетам стороны 2
deviation_amount = |ΣA − ΣB|; deviation_percent = deviation_amount / min(ΣA, ΣB) × 100
элиминация = min(ΣA, ΣB) при расхождении ≤ 5 % (раздел 3.3); остатки сторон уменьшаются на неё
ΣA = Σ строк A с контрагентом B по счетам стороны 1 (с исключениями), со знаком
ΣB = Σ строк B с контрагентом A по счетам стороны 2, со знаком
deviation_amount = |ΣA − ΣB|; deviation_percent = deviation_amount / min(ΣA, ΣB) × 100
элиминация = min(ΣA, ΣB) при расхождении ≤ 5 % (раздел 3.3)
```
Зачем каскад: счета правил пересекаются (дебиторка у № 5–8 общая; 67.03 входит и в № 3, и в маску 67 у № 7; 66.03 — в
№ 4 и в № 8). По буквальным формулам Прил. 3 одна дебиторка элиминировалась бы несколько раз. Порядок `rule_no` в Прил. 3
уже идёт от частного к общему (№ 3/4 раньше № 7/8, № 5 раньше № 6, № 6 → 7 → 8), а сам принцип «списывать по очереди до
обнуления» Прил. 3 задаёт в правиле № 1.
Суммы — **со знаком**, как в шахматке (решение владельца 23.10.2026): отрицательное сальдо строки — встречная позиция
(аванс, переплата), у A по B строки 150 и −50 дают ΣA = 100.
Пример: у A дебиторка на B 100; у B кредиторка на A 70 и долгосрочный заём (67) на A 50. Буквально: № 6 = 70,
№ 7 = 50, итого 120 — больше самой дебиторки. Каскадом: № 6 = 70, остаток A = 30; № 7 = min(30, 50) = 30; итого 100;
20 по 67 у B — расхождение, разбирается корректировкой.
Счета правил пересекаются (дебиторка у № 5–8 общая; 67.03 входит и в № 3, и в маску 67 у № 7; 66.03 — в № 4 и в № 8),
поэтому строка такого счёта пока учитывается в **каждом** подходящем правиле, и сумма по паре компаний может превысить
сумму стороны. Двойной учёт снимается разнесением строк по правилам и ручным переносом между правилами (AURORA-1751).
**Упаковка строк** — какие строки ОСВ с какими сопоставлены (детализация и строки без пары). Правило охватывает
несколько счетов, поэтому строки сопоставляются группами **1:1, 1:N, N:1**; N:M нет, N ≤ 5. Выбирается **минимальная
упаковка** — группа с меньшим числом строк: если сумму закрывают и «1 Дт ↔ 2 Кт», и «1 Дт ↔ 3 Кт», берётся 1:2.
```
правила пары — по возрастанию rule_no; строка, сопоставленная в раннем правиле, в следующих не участвует
каждое правило — отдельно; строки с суммой > 0 по счетам правила
строки с отрицательной суммой → «без пары», разбирает аналитик; с нулевой → «без пары»
для k = 2 … 6 (число строк группы: 1:1, затем 1:2 и 2:1, … до 1:5 и 5:1):
кандидаты размера k из ещё не сопоставленных строк, закрывающие сумму:
сначала точные, затем с расхождением ≤ 5 % к меньшей — по возрастанию расхождения
сначала точные, затем с расхождением ≤ 5 % к меньшей — по возрастанию процента расхождения
группа без соперников → пара, её строки выбывают
две и более группы с тем же k и тем же расхождением, делящие строку → на ручное (между правилами ничьей нет)
строки, не сопоставленные ни одним правилом → «без пары»
две и более группы с тем же k и тем же процентом, делящие строку → на ручное
строки, не вошедшие ни в одну группу → «без пары»
```
Способ упаковки сумму элиминации не меняет: в КФО идёт агрегат тройки.
@ -184,8 +183,8 @@ DBML и `05-domain-rules.md`, а этот документ поправь сле
| нет второй стороны (ΣB = 0) | элиминируется 0, строки — «без пары» (вопрос В-4) |
| правило с `mode = manual` (№ 1, № 2) | автоматически не элиминируется, только корректировкой / гудвиллом |
Суммы сторон — остатки после предыдущих правил пары (каскад, раздел 3.2), поэтому двойной элиминации по общим счетам
нет.
Каждое правило считается отдельно, поэтому строка общего счёта пока входит в суммы нескольких правил (раздел 3.2) —
двойной учёт снимается разнесением строк (AURORA-1751).
Алгоритм — по ТЗ (решение владельца 08.10.2026, OQ-155): пары, `min`, порог 5 %. Эталонный Excel «Консолидация_Группа
РБ» элиминирует у каждой компании полную сумму её строк на группу — это сознательное расхождение с эталоном.
@ -300,7 +299,7 @@ vgo_amount = eliminated_auto + eliminated_manual
**Сумма элиминации (правило № 6):** ΣA = 110 429 113,15, ΣB = 106 311 116,46, расхождение 3,87 % ≤ 5 % →
элиминируется `min` = 106 311 116,46: статья 1230 у Счастливочной, 1520 у РБ. Это при условии, что у РБ нет 66.04 / 67.04
на Счастливочную: иначе по каскаду 76.06 и 76.АВ сначала уйдут в правило № 5, и ΣA правила № 6 уменьшится.
на Счастливочную: иначе 76.06 и 76.АВ войдут и в правило № 5 — двойной учёт до разнесения строк (AURORA-1751).
**76.АВ** (НДС по авансам) сейчас входит в дебиторку по правилу «76 кроме 76.07.5/76.07.9». Эталон его исключает —
вопрос OQ-164 (В-9).
@ -325,4 +324,4 @@ vgo_amount = eliminated_auto + eliminated_manual
| В-9 | 76.АВ / 76.ВА (НДС по авансам) — исключать из ДЗ / КЗ? Есть ли ещё такие субсчета 76? | OQ-164 открыт, вопрос в AURORA-1373 |
| В-10 | Округление при элиминации: рубли → тыс. руб. на выходе, ROUND_HALF_UP? | допущение OQ-26 |
| В-11 | Правило № 1: если у компании А нет ОСВ по 03 — результат в строку «ППА»? | открыто в §4 |
| В-12 | ~~Счета правил пересекаются — как избежать двойной элиминации?~~ | **решено 19.10.2026 (OQ-173):** каскад по остатку в порядке `rule_no` (раздел 3.2) |
| В-12 | ~~Счета правил пересекаются — как избежать двойной элиминации?~~ | **решено 21.10.2026:** каскад по остатку отменён, правила считаются отдельно, двойной учёт снимает разнесение строк по правилам (AURORA-1751, раздел 3.2) |

View File

@ -76,7 +76,7 @@ git remote add origin <url-arm-core> && git push -u origin main
| Слой | Где | Статус | Правило для агента |
|---|---|---|---|
| Модель данных | `docs/model/data-model-v5.22.dbml` | стабильный, версионируется | единственный источник имён и структур |
| Модель данных | `docs/model/data-model-v5.23.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 → вопрос, не код |

View File

@ -1,4 +1,4 @@
# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.22)
# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.23)
## Компоненты (= репозитории, см. `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.22; имена таблиц/колонок/enum — как в DBML
models/ # ORM 1:1 с DBML v5.23; имена таблиц/колонок/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/`, имя файла стандартное
`<rev>_<slug>.py`. Автогенерация — только черновик: итоговую ревизию сверяет с DBML `api/scripts/check_dbml_vs_orm.py`.
- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.22.
- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.23.
### Инфраструктурные клиенты

View File

@ -1,6 +1,6 @@
# 04. Как читать модель данных v5.22
# 04. Как читать модель данных v5.23
Файлы: `docs/model/data-model-v5.22.dbml` (источник истины), `docs/model/entities-v5.22.md` (смысл таблиц; предыдущие
Файлы: `docs/model/data-model-v5.23.dbml` (источник истины), `docs/model/entities-v5.23.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`, `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.23:** `consolidation_version.elimination_rules_version` — `not null`: версия правил элиминации, действующая на дату создания версии консолидации, фиксируется при создании; существующим версиям проставляется миграцией (AURORA-1536).
**v5.22:** из описания `comment.object_type` убраны `chess_cell` и `kfo_result_line`, сохранены `document | consolidation_version | …`; структура БД не меняется, миграция не требуется (AURORA-1446).
**v5.21:** повторная обработка файла пишется в журнал своим событием `audit_action.FILE_REPROCESS` (раздел

View File

@ -496,9 +496,10 @@ closing_* = closing_* строки Qn
Хранятся как данные: `elimination_rule` (+ `elimination_rule_account`), версионируются; версия фиксируется в
`consolidation_version.elimination_rules_version`. Для версии консолидации берётся версия правил, **действующая на дату
составления консолидации** (создания версии), а не на её отчётный период; фиксируется при создании версии или первом
расчёте, не в GET-запросе (OQ-174, решение владельца 21.10.2026; реализация — AURORA-1536). Пока поле пусто, расчёты
берут открытые правила (`valid_to IS NULL`). Seed-набор для ББ (`statement_type = balance`, `measure = closing`):
составления консолидации** (создания версии), а не на её отчётный период; фиксируется при создании версии и дальше не
меняется (OQ-174, решение владельца 21.10.2026). С DBML v5.23 поле обязательно (решение владельца 23.10.2026, AURORA-1536):
версия консолидации без действующих правил не создаётся, у версий, созданных раньше, номер проставлен миграцией по дате
создания. Seed-набор для ББ (`statement_type = balance`, `measure = closing`):
| № | Статья А (Дт) | Счета А (closing_debit) | Статья Б (Кт) | Счета Б (closing_credit) | mode | Примечание |
|---|---|---|---|---|---|---|
@ -530,10 +531,11 @@ closing_* = closing_* строки Qn
---
## §5. Автоматическое сопоставление ВГО (DOC-223044; OQ-07, OQ-173 — решения владельца 19.10.2026)
## §5. Автоматическое сопоставление ВГО (DOC-223044; OQ-07, OQ-173 — решения владельца 19.10, 21.10 и 23.10.2026)
Границы: только `closing_debit` / `closing_credit`; только компании периметра версии; правила и счета сторон — из §4
(версия правил — `consolidation_version.elimination_rules_version`); строки `group_type = COUNTERPARTY`,
(версия правил — `consolidation_version.elimination_rules_version`), только правила ББ (`statement_type = balance`,
`measure = closing`; правило с другим показателем — ошибка `INVALID_RULE`); строки `group_type = COUNTERPARTY`,
`data_type ∈ (BU, SUM)`. Единица расчёта — **тройка** (правило r, компания A, компания B). Правила считаются для каждой
упорядоченной пары компаний — и (A, B), и (B, A); поэтому зеркальные правила № 9–16 Прил. 3 в seed не нужны (№ 14 для
(A, B) — это № 6 для (B, A)).
@ -541,53 +543,56 @@ closing_* = closing_* строки Qn
Два уровня: **сумма элиминации** по тройке — агрегат, идёт в КФО; **упаковка** — какие строки ОСВ с какими сопоставлены
(детализация, строки без пары). Способ упаковки сумму элиминации не меняет (решение владельца 19.10.2026).
**1. Сумма элиминации по тройке — каскадом по остатку** (AURORA-1536; OQ-173). Счета правил пересекаются (дебет
№ 5–8 общий; кредит 67.03 — № 3 и маска 67 у № 7, 66.03 — № 4 и маска 66 у № 8), и по буквальным формулам Прил. 3 одна
и та же сумма элиминировалась бы несколькими правилами. Поэтому правила пары (A, B) считаются **по порядку `rule_no`**, и
каждое берёт min из **остатка** сторон после предыдущих — тот же принцип, что Прил. 3 задаёт в правиле № 1 («сначала из
1450 до обнуления, остаток — из 1150, затем из 1550»). Порядок `rule_no` в Прил. 3 уже идёт от частного к общему:
№ 3/4 (58.03 ↔ 67.03 / 66.03) раньше № 7/8 (↔ 67 / 66), № 5 (76 ↔ проценты 66.04/67.04) раньше № 6 (↔ кредиторка),
№ 6 → № 7 → № 8 (дебиторка гасится кредиторкой, потом долгосрочными, потом краткосрочными займами). Отдельное поле
порядка не нужно.
**1. Сумма элиминации по тройке — каждое правило отдельно.** Каскад по остатку в порядке `rule_no` **отменён** (решение
владельца 21.10.2026 по созвону с заказчиком 19.10.2026, OQ-173): счета правил пересекаются (дебет № 5–8 общий; кредит
67.03 — № 3 и маска 67 у № 7, 66.03 — № 4 и маска 66 у № 8), и строка такого счёта пока входит в Σ **каждого** подходящего
правила. Двойной учёт снимает разнесение строк по правилам и ручной перенос между правилами (AURORA-1751); до него
сумма элиминации по паре компаний может превышать сумму стороны — это известное ограничение, не дефект.
```
остаток(строка) = сумма строки (сторона side_1 у A по B, противоположная у B по A)
для r по возрастанию rule_no:
ΣA = Σ остатка строк A с counterparty_id = B по счетам стороны 1 правила r (с исключениями is_excluded)
ΣB = Σ остатка строк B с counterparty_id = A по счетам стороны 2 правила r
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)
остаток строк правила r на каждой стороне уменьшается на элиминация_r (по строкам — в порядке упаковки п. 2,
затем нераспределённые; на сумму КФО порядок внутри стороны не влияет)
ΣA = Σ строк A с counterparty_id = B по счетам стороны 1 правила r (с исключениями is_excluded), сумма стороны side_1
ΣB = Σ строк B с counterparty_id = A по счетам стороны 2 правила r, противоположная сторона
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)
```
Пример: у A дебиторка на B 100; у B кредиторка на A 70 и долгосрочный заём (67) на A 50. Буквально по Прил. 3:
№ 6 = 70, № 7 = 50, итого 120 — больше самой дебиторки. Каскадом: № 6 = min(100, 70) = 70, остаток A = 30;
№ 7 = min(30, 50) = 30; итого 100; 20 по 67 у B — расхождение пары, разбирается корректировкой.
**Знак** (решение владельца 23.10.2026): суммы сторон — **со знаком**, как в шахматке §6 (одно правило и одна пара дают
одинаковые ΣA и ΣB на обоих экранах). Отрицательное сальдо строки (§1: возможно) — встречная позиция, например аванс или
переплата: у A по B строки 150 и −50 дают ΣA = 100, а не 200. Если `min(ΣA, ΣB) ≤ 0` (одна сторона пуста или ушла в
минус), процент не считается (`null`), элиминация 0, тройка подсвечивается при любом расхождении — разбирает аналитик
(в шахматке тот же случай — OQ-175).
**2. Упаковка строк в тройке** — группами **1:1, 1:N, N:1** (правило охватывает несколько счетов, поэтому одной
строке на одной стороне могут соответствовать несколько строк на другой). N:M нет. Выбирается **минимальная упаковка** —
группа с меньшим числом строк: если сумму закрывают и «1 Дт ↔ 2 Кт», и «1 Дт ↔ 3 Кт», берётся 1:2.
группа с меньшим числом строк: если сумму закрывают и «1 Дт ↔ 2 Кт», и «1 Дт ↔ 3 Кт», берётся 1:2. Каждое правило
упаковывается независимо (каскада нет): строка может войти в пары разных правил, пока её не разнесли (AURORA-1751).
```
правила пары (A, B) — по возрастанию rule_no; строка, сопоставленная в раннем правиле, в следующих не участвует
вход правила r: ещё не сопоставленные строки a стороны 1 (A → B) и b стороны 2 (B → A) по счетам правила r
вход правила r: строки a стороны 1 (A → B) и b стороны 2 (B → A) по счетам правила r с суммой > 0
строки с суммой < 0 в упаковку не идут → «без пары», причина NEGATIVE_AMOUNT (разбирает аналитик)
строки с суммой 0 → «без пары», причина ZERO_AMOUNT
для k = 2 … 6 (число строк группы: 1:1, затем 1:2 и 2:1, … до 1:5 и 5:1; N ≤ 5):
кандидаты размера k из ещё не сопоставленных строк, «закрывающие» сумму:
точные (Σ одной стороны == Σ другой), затем с расхождением ≤ 5 % к меньшей — по возрастанию расхождения
точные (Σ одной стороны == Σ другой), затем с расхождением ≤ 5 % к меньшей — по возрастанию ПРОЦЕНТА расхождения
(решение владельца 23.10.2026)
группа без соперников → пара, её строки выбывают
две и более группы с тем же k и тем же расхождением, делящие строку → не сопоставляем, на ручное
(между разными правилами ничьей нет — её решает порядок rule_no)
строки, не сопоставленные ни одним правилом → «без пары»
результат пары: rule, A, B, osv_entry.id обеих сторон, Σ A, Σ B, deviation_amount, deviation_percent
две и более группы с тем же k и тем же процентом расхождения, делящие строку → не сопоставляем, на ручное (AMBIGUOUS)
строки, не вошедшие ни в одну группу → «без пары» (NO_MATCH)
результат группы: rule, A, B, osv_entry.id обеих сторон, Σ A, Σ B, deviation_amount, deviation_percent
```
Результат **не хранится** (DECISION-5) — возвращается как структура для шахматки, корректировок и КФО. Ручное решение по
паре — `correction` с естественным ключом (`rule_id`, `legal_entity_id`, `counterparty_id`).
**3. Выдача.** Два запроса (контракт — `dev/frontend-api-guide.md`): **список пар версии** — все тройки «правило ББ ×
упорядоченная пара компаний периметра», где хотя бы одна сторона не ноль, с ΣA, ΣB, расхождением, процентом, суммой
элиминации, статусом (сопоставлено автоматически / с расхождением > 5 % / без встречной стороны / ручное правило) и
`osv_entry.id` строк обеих сторон; сводная ОСВ строится один раз на запрос. **Детализация тройки** — упаковка п. 2:
точные группы, группы с расхождением ≤ 5 %, строки без пары с причиной.
**Алгоритм — по ТЗ** (решение владельца 08.10.2026, OQ-155): пары, `min`, порог 5 %, а не полная элиминация каждой
стороны, как в эталонном Excel. Пример на фикстурах (Q3 2024, правило № 6): у Счастливочной на «Русский бисквит» 62.01
«БП Производство» 106 311 116,46, 62.01 «БП Аренда» 1 401 330, 76.06 350 000, 76.АВ 2 366 666,69; у РБ — одна строка
60.01 106 311 116,46. Упаковка: точная пара 1:1 (106 311 116,46), остальные три строки Счастливочной — без пары.
Сумма элиминации: ΣA = 110 429 113,15, ΣB = 106 311 116,46, расхождение 3,87 % ≤ 5 % → min = 106 311 116,46 (если у РБ нет
66.04/67.04 на Счастливочную; иначе 76.06 и 76.АВ сначала уйдут в правило № 5 и ΣA правила № 6 уменьшится).
Сумма элиминации: ΣA = 110 429 113,15, ΣB = 106 311 116,46, расхождение 3,87 % ≤ 5 % → min = 106 311 116,46. Строки 76.06
и 76.АВ входят и в правило № 5, если у РБ есть 66.04/67.04 на Счастливочную, — двойной учёт до AURORA-1751.
---

View File

@ -178,7 +178,7 @@
| OQ-170 | Состав `counterparty_accounts` (контрагенты расчётных счетов без названия вида, §1.3): исключены 71 «Расчёты с подотчётными лицами» и 73 «Расчёты с персоналом» (физлица, OQ-158), список — 58, 60, 62, 66, 67, 75, 76, 80. Верно ли исключение; нужны ли 68, 69, 70, 77 и др. | tabular-parser #59, change `osv-flat-form` (R-9, решение сессии при одобрении запуска 19.10.2026) | §1.3, сопоставление контрагентов (OQ-158) | открыт; допущение принято: 71/73 исключены (аналитика без названия вида на них остаётся `OTHER`, в сопоставление не попадает) |
| OQ-171 | Вид субконто «Организации» в строке между названием ведомости и периодом (файл-письмо «ОСВ сч. 60 за 2 кв. 2026», план счетов ООО Хреновская Нива): что это за вид в 1С — организации-контрагенты (справочник «Организации» или «Контрагенты» под другим названием) или собственные организации учёта; на каких счетах встречается | tabular-parser #59, change `osv-flat-form` (R-10, решение сессии при одобрении запуска 19.10.2026) | §1.1 (синоним в `osv_groups.yaml`), сопоставление контрагентов | открыт; допущение принято: синоним `COUNTERPARTY` — только множественное число «Организации» (`^организации\b`, R-17); единственное «Организация» (собственная организация учёта в шапке) остаётся `OTHER`; другой смысл «Организации» на иных счетах даст `COUNTERPARTY` вместо `OTHER`, виден по предупреждениям |
| OQ-172 | Безымянная аналитика первого уровня на счёте **63** (резервы по сомнительным долгам) в плоской форме без названия вида: на файлах контура таких строк нет; 51 строка — в fixtures, golden `osv-dkb/nm/2025-Q3/НМ_ОСВ_63_Контрагенты_…json` (имя файла «63_Контрагенты» — довод за включение 63); счёт 63 не входит в `counterparty_accounts` (§1.3), строки остаются `OTHER` без имени. Нужен ли 63 в списке (аналитика резерва — контрагенты-должники) | tabular-parser #59, приёмка Task 6 (19.10.2026) | §1.3, сопоставление контрагентов | открыт; допущение принято: 63 в список не входит, строки `OTHER` |
| OQ-173 | Пересечение правил в сумме элиминации: счета сторон правил §4 пересекаются — дебет № 5–8 (76 у № 5; 60/62/76 у № 6–8), кредит: 67.03 (№ 3 и маска 67 у № 7), 66.03 (№ 4 и маска 66 у № 8), 66.04/67.04 (№ 5 и № 7/8). Строка такого счёта входит в Σ стороны каждого правила, и сумма min(ΣA, ΣB) по правилам может превысить Σ самой стороны — двойная элиминация. Считать ΣA правила только по строкам, которые упаковка (§5 п. 2) отнесла к этому правилу? Или распределять сторону по правилам в фиксированном порядке? | решение по OQ-07 (docs #136, 19.10.2026), описание AURORA-1536 | §5 п. 1, AURORA-1536, КФО (§8 `eliminated_auto`) | **закрыт решением владельца 19.10.2026 (docs #136):** каскад по остатку — правила пары (A, B) считаются по порядку `rule_no` (в Прил. 3 он уже от частного к общему: № 3/4 раньше № 7/8, № 5 раньше № 6, № 6 → 7 → 8), каждое берёт min из остатка сторон после предыдущих, как в правиле № 1 Прил. 3 («сначала из 1450 до обнуления…»); упаковка — в том же порядке, строка сопоставляется одним правилом; ничьей между правилами нет. Поле порядка не нужно. Алгоритм — `05-domain-rules.md` §5 |
| OQ-174 | Какая версия правил элиминации (§4) применяется к версии консолидации, если `consolidation_version.elimination_rules_version` не задан: открытые на сегодня, действующие на отчётную дату консолидации или на дату её составления; когда фиксировать номер версии | ревью arm-core PR #141 (21.10.2026) | §4, §5, AURORA-1536 | **закрыт решением владельца 21.10.2026 (docs #145):** версия правил, действующая на дату составления консолидации (создания версии), а не на отчётный период; фиксируется в `elimination_rules_version` при создании версии или первом расчёте, не в GET-запросе; реализация — AURORA-1536; до неё расчёты берут открытые правила (`valid_to IS NULL`, сейчас одна версия v1) |
| 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 со знаком) |
| OQ-173 | Пересечение правил в сумме элиминации: счета сторон правил §4 пересекаются — дебет № 5–8 (76 у № 5; 60/62/76 у № 6–8), кредит: 67.03 (№ 3 и маска 67 у № 7), 66.03 (№ 4 и маска 66 у № 8), 66.04/67.04 (№ 5 и № 7/8). Строка такого счёта входит в Σ стороны каждого правила, и сумма min(ΣA, ΣB) по правилам может превысить Σ самой стороны — двойная элиминация. Считать ΣA правила только по строкам, которые упаковка (§5 п. 2) отнесла к этому правилу? Или распределять сторону по правилам в фиксированном порядке? | решение по OQ-07 (docs #136, 19.10.2026), описание AURORA-1536 | §5 п. 1, AURORA-1536, КФО (§8 `eliminated_auto`) | **закрыт решением владельца 19.10.2026 (docs #136):** каскад по остатку — правила пары (A, B) считаются по порядку `rule_no` (в Прил. 3 он уже от частного к общему: № 3/4 раньше № 7/8, № 5 раньше № 6, № 6 → 7 → 8), каждое берёт min из остатка сторон после предыдущих, как в правиле № 1 Прил. 3 («сначала из 1450 до обнуления…»); упаковка — в том же порядке, строка сопоставляется одним правилом; ничьей между правилами нет. Поле порядка не нужно. **Пересмотрено 21.10.2026** (решение владельца по созвону с заказчиком 19.10.2026): каскад отменён, каждое правило считается и упаковывается отдельно, строка общего счёта пока входит в несколько правил; двойной учёт снимает разнесение строк по правилам и ручной перенос (AURORA-1751). Алгоритм — `05-domain-rules.md` §5 |
| OQ-174 | Какая версия правил элиминации (§4) применяется к версии консолидации, если `consolidation_version.elimination_rules_version` не задан: открытые на сегодня, действующие на отчётную дату консолидации или на дату её составления; когда фиксировать номер версии | ревью arm-core PR #141 (21.10.2026) | §4, §5, AURORA-1536 | **закрыт решением владельца 21.10.2026 (docs #145):** версия правил, действующая на дату составления консолидации (создания версии), а не на отчётный период; фиксируется в `elimination_rules_version` при создании версии, не в GET-запросе; реализация — AURORA-1536. С DBML v5.23 (решение владельца 23.10.2026, docs #164) поле обязательно: у версий, созданных раньше, номер проставлен миграцией по дате создания |
| 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: не трогать) |

View File

@ -48,8 +48,8 @@
| Файл | Что |
|---|---|
| `data-model-v5.22.dbml` | источник истины; changelog v5.1…v5.22 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.21 остаются рядом как история) |
| `entities-v5.22.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.21 остаются как история) |
| `data-model-v5.23.dbml` | источник истины; changelog v5.1…v5.23 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.22 остаются рядом как история) |
| `entities-v5.23.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.22 остаются как история) |
| `diagrams/part01…09.{svg,png}` | диаграммы v5.21 по частям; перерисовываются `make diagrams` (`scripts/gen_model_diagrams.py`) |
| `source/tz/data-model-part1_4.dbml` (лежит не в `model/`, а в экспорте ТЗ) | v4 часть 1 — историческое |

View File

@ -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.22.dbml` и
`docs/model/entities-v5.22.md`.
3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.23.dbml` и
`docs/model/entities-v5.23.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.22; поля, которых нет в DBML, не изобретать;
- имена таблиц, колонок и enum'ов — ровно как в DBML v5.23; поля, которых нет в DBML, не изобретать;
- алгоритмы — только из `05-domain-rules.md`; из страниц ТЗ напрямую не кодировать;
- парсеры: никаких координат ячеек, букв колонок, имён листов и номеров строк в коде — структура по содержимому,
синонимы в `rules/*.yaml`; фикстуры — регрессия, не спецификация;

View File

@ -620,6 +620,36 @@ variance_percent?, exceeds_threshold, one_sided, skipped_osv_entry_count, osv_en
счета» — без забалансовых 001–012. `skipped_osv_entry_count` —
строки ОСВ пары без сопоставленного счёта, в суммы они не вошли. Выгрузки пишут в журнал `EXPORT`.
### 5.7.2 Пары ВГО и сумма элиминации (Срез 5, правила — `05-domain-rules.md` §5; arm-core #175, AURORA-1536)
Префикс — `/consolidations/versions/{version_id}`. Права — `CONSOLIDATION_VIEW` и policy версии (чужая — 404); блокировка не
нужна. Считается на лету по сводной ОСВ версии и правилам ББ зафиксированной версии правил (`elimination_rules_version`,
с DBML v5.23 есть у каждой версии), ничего не хранится. Каждое правило считается отдельно: строка общего счёта может войти в
несколько правил (двойной учёт снимает AURORA-1751).
| Метод и путь | Запрос | Ответ | Ошибки |
|---|---|---|---|
| `GET …/elimination-totals` | — | `EliminationTotalsOut` — все тройки версии: правила ББ × упорядоченные пары компаний периметра, у которых есть строки хотя бы на одной стороне | 404 |
| `GET …/elimination-pairs?rule_id=&company_a_id=&company_b_id=` | три параметра обязательны | `EliminationPairsOut` — итог тройки и упаковка строк | 404 (версия; правило не из версии правил); 422 `INVALID_COMPANY_PAIR` (A = B или не из периметра), `INVALID_RULE` (не ББ, показатель не `closing`), `ACCOUNTS_REQUIRED` |
`EliminationTotalsOut` — `{version_no, items[] {rule_id, rule_no, elimination_mode, company_a_id, company_b_id, amount_a,
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.
`osv_entry_ids_*` — строки ОСВ, вошедшие в ΣA и ΣB (по ним — переход в сводную ОСВ).
`EliminationPairsOut` — `{version_no, rule_id, rule_no, company_a_id, company_b_id, amount_a, amount_b, deviation_amount,
deviation_percent?, eliminated_amount, exceeds_threshold, matched[], with_discrepancy[], unpaired[]}`. Группы упаковки
(`matched` — точные, `with_discrepancy` — расхождение ≤ 5 %, по возрастанию процента) — `{rule_id, company_a_id, company_b_id,
amount_a, amount_b, deviation_amount, deviation_percent, osv_entry_ids_a[], osv_entry_ids_b[]}`; `unpaired[]` — `{row {entry_ids[],
legal_entity_id, counterparty_id, account_id, account_code, closing_debit?, closing_credit?}, reason}`, `reason`: `NO_MATCH` —
не нашлось группы, `AMBIGUOUS` — две равные группы делят строку (на ручное), `ZERO_AMOUNT`, `NEGATIVE_AMOUNT` — отрицательная
сумма строки, в упаковку не идёт.
Суммы — строки в рублях с двумя знаками, со знаком (как в шахматке); `deviation_amount = |ΣA − ΣB|`, `deviation_percent` —
к меньшей стороне, `null`, если она ≤ 0. Упаковка сумму элиминации не меняет: в КФО идёт `eliminated_amount` тройки.
### 5.8 Схемы ответов (кратко)
Поля с `?` могут быть `null`, но ключ присутствует всегда. Полные схемы — `/openapi.json` и контракты по областям.

1546
model/data-model-v5.23.dbml Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 517 KiB

View File

@ -370,7 +370,9 @@
<polygon fill="#e7e2dd" stroke="transparent" points="936.7,-1233.68 936.7,-1293.68 1502.7,-1293.68 1502.7,-1233.68 936.7,-1233.68"/>
<polygon fill="none" stroke="#29235c" points="936.7,-1233.68 936.7,-1293.68 1502.7,-1293.68 1502.7,-1233.68 936.7,-1233.68"/>
<text text-anchor="start" x="947.7" y="-1253.88" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">elimination_rules_version &#160;&#160;&#160;</text>
<text text-anchor="start" x="1457.91" y="-1254.88" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">int</text>
<text text-anchor="start" x="1418.82" y="-1254.88" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">int</text>
<text text-anchor="start" x="1452.61" y="-1254.88" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
<text text-anchor="start" x="1461.5" y="-1254.88" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
<polygon fill="#e7e2dd" stroke="transparent" points="936.7,-1173.68 936.7,-1233.68 1502.7,-1233.68 1502.7,-1173.68 936.7,-1173.68"/>
<polygon fill="none" stroke="#29235c" points="936.7,-1173.68 936.7,-1233.68 1502.7,-1233.68 1502.7,-1173.68 936.7,-1173.68"/>
<text text-anchor="start" x="947.7" y="-1193.88" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">is_in_work &#160;&#160;&#160;</text>

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -287,7 +287,7 @@
<!-- comment -->
<g id="comment" class="node">
<title>comment</title>
<g id="a_comment"><a xlink:title="comment&#10;ADD ТЗ 6.3/6.4: комментарии к отчётности и КФО (в т.ч. обязательные при расхождениях)">
<g id="a_comment"><a xlink:title="comment&#10;ТЗ 6.3; AURORA&#45;1446: комментарии к документу. Правка и удаление комментариев не предусмотрены; аудит не ведётся">
<ellipse fill="none" stroke="black" stroke-width="0" cx="1160.3" cy="-3597.68" rx="299.21" ry="299.63"/>
<polygon fill="#1d71b8" stroke="transparent" points="951.3,-3747.68 951.3,-3807.68 1370.3,-3807.68 1370.3,-3747.68 951.3,-3747.68"/>
<polygon fill="none" stroke="#29235c" points="951.3,-3747.68 951.3,-3807.68 1370.3,-3807.68 1370.3,-3747.68 951.3,-3747.68"/>

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

246
model/entities-v5.23.md Normal file
View File

@ -0,0 +1,246 @@
# АРМ КА — описание сущностей модели данных v5.23
Источник истины по структуре — `data-model-v5.23.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…09 svg/png; перерисовываются `make diagrams` из текущего DBML). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась.
## Что изменилось в 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 |
| `goodwill` | Строка расчёта гудвилла: головная × дочерняя, доля владения, ФВ по 58.01, УК (1310) и добавочный капитал (1350 — вопрос методологам), гудвилл, «включить в КФО», комментарий. | → `consolidation_version`, `legal_entity` ×2, `app_user` | ТЗ 6.7, Прил. 3 п.2, Прил. 8 |
| `kfo_result` | Шапка результата КФО: когда посчитан, заморожен ли, ЧА и ВБ, выручка, прибыль, расхождения, пороги, обоснование, итог гудвилла. | → `consolidation_version` (1:1); ← `kfo_result_line` | ТЗ 6.4, 6.8, Прил. 10 |
| `kfo_result_line` | Строка консолидированного ББ/ОФР по коду ФСБУ 4/2023: сумма → авто-элиминация → корректировки → гудвилл → итог; «в т.ч. ВГО», источник, расхождение, комментарий. | → `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`.

View File

@ -1,12 +1,12 @@
schema: spec-driven
context: |
Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.22), правила домена, контракты между сервисами
Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.23), правила домена, контракты между сервисами
(docs/contracts/*.schema.json), макет UI и экспорт ТЗ системы АРМ КА (ДКБ, Россельхозбанк).
Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + проверки качества (make check) + изменения
документов контекста. Код сервисов живёт в соседних репозиториях (../arm-core, ../tabular-parser, ../scan-gateway),
у каждого свой openspec/ и свой change; сквозной change ссылается на них.
Источники истины по приоритету: docs/model/data-model-v5.22.dbml → docs/model/entities-v5.22.md →
Источники истины по приоритету: docs/model/data-model-v5.23.dbml → docs/model/entities-v5.23.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/** не редактируется.

View File

@ -16,7 +16,7 @@
- NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`.
## Правила репозитория
- ORM 1:1 с `../docs/model/data-model-v5.22.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI.
- ORM 1:1 с `../docs/model/data-model-v5.23.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI.
- Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring.
- Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц.
- Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.