arm-ka-docs/dev/api-contract-corrections.md

347 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Контракт API таблицы корректировок (AURORA-1540 / AURORA-1541)
Черновик контракта для бэкенда (AURORA-1540) и фронта (AURORA-1541, arm-core #206). Машиночитаемая копия —
`dev/api-contract-corrections.openapi.yaml`; при расхождении двух файлов правится тот, что ошибся, и оба держатся одинаковыми.
Источники, в порядке приоритета:
1. Аналитика Лысенко: DOC-272613 «Корректировка данных агрегатом», ответы на вопросы 1–7 (комментарий от 27.10.2026 в
AURORA-1395), ответы на `[design-23-10-check]` (комментарий #9920570 от 29.10.2026 там же), DOC-274230 «Правила пар
счетов».
2. Дизайн таблицы корректировок от 23.10.2026 (вложение к DOC-272613).
3. ТЗ 6.8, Прил. 9 — только там, где аналитика молчит.
На `[design-23-10-check]` (12 пунктов + 3 по ТЗ) Лысенко ответил 29.10; ссылки вида «ответ п. N» — на этот комментарий.
Открыта граница ровно 5 % (§2); ОФР (AURORA-1396, AURORA-1999) реализован — §3.5 и DOC-277424. Разделение работ с бэкендом — комментарии `[allocation-split]` в
AURORA-1540 и AURORA-1751 (29.10): разнесение строк и решения по строкам — 1751, корректировка и таблица — 1540.
## 1. Общее
Всё как в `dev/api-contract-slice-4.md` §1:
- префикс `/api/v1`, `Authorization: Bearer <JWT>`;
- чтение — право `CONSOLIDATION_VIEW`, правки — `CONSOLIDATION_EDIT`;
- версия вне видимости — 404 `NOT_FOUND`, как несуществующая;
- доменные ошибки — `{"detail": {"code", "message", ...}}`, 422 валидации тела — массив pydantic;
- **деньги — строки с двумя знаками** (`"1000000.00"`), всегда в **рублях** (суммы ОСВ); перевод в тыс. руб. и округление
для показа — на фронте;
- проценты — строки с двумя знаками (`"394.27"`), `null` — процент не определён.
**Правки.** Порядок проверок: право (403) → видимость (404) → версия редактируема (409) → операция. Какие статусы версии
разрешают корректировки, решает сервер (по §9; статус «Верифицирован» и закрытые — только чтение, подтверждено ответом по ТЗ); фронт не повторяет
это правило, а читает `editable` / `read_only_reason` из ответа таблицы. Ошибки правок версии — те же коды, что везде:
409 `VERSION_NOT_EDITABLE`, `LOCK_REQUIRED`, `LOCKED_BY_OTHER`.
## 2. Понятия
- **Строка** — неделимая строка сводной ОСВ версии (как `ConsolidatedRowOut`): `entry_ids` — её исходные `osv_entry`.
В разнесении участвуют строки уровня «счёт — контрагент», владелец и контрагент — компании периметра, тип данных — БУ
(DOC-272613 §1). Для ББ сумма строки — сальдо на конец, для ОФР — обороты по правилу (`measure`). Строка ОФР всегда даёт
две независимые позиции — `side = "D"` (`turnover_debit`) и `side = "K"` (`turnover_credit`); решения по ним хранятся
раздельно (§3.5). У ББ решение одно на обе позиции строки, `side` не передаётся.
- **Агрегат** — правило × компания 1 × компания 2 (× тип отчётности, он следует из правила). Отдельно не хранится, считается
при каждом запросе (DOC-272613 §3). Ключ агрегата — `(rule_id, company_1_id, company_2_id)`.
- **Разнесение** (DOC-272613 «Распределение строк по правилам», ответы 1(б), 2(а)) — чистая функция от версии консолидации,
считается на каждый запрос и не хранится (DECISION-5); хранятся только ручные решения аналитика (`vgo_assignment`, §5):
1. ручное решение по строке (`vgo_assignment`) важнее автоматики;
2. строка подходит под одно правило — в это правило, даже без встречной стороны;
3. под несколько правил и у второй компании есть строки по счетам другой стороны ровно одного из них — в это правило;
4. пара есть в нескольких правилах — в правило с меньшим номером Прил. 3 (ответ 2(а));
5. иначе — нераспределённая строка.
Каждая строка учитывается не более чем в одном агрегате своего типа отчётности.
- **Сторона агрегата.** Сторона 1 — строки компании 1 по счетам стороны 1 правила, сторона 2 — строки компании 2 по счетам
стороны 2. Сумма стороны — сумма всех её строк: агрегат учитывается в КФО целиком, лишнюю строку убирают из правила, и она
становится нераспределённой (ответ п. 4).
- **«Не учитывать в КФО»** — отметка только у нераспределённой строки (ответ п. 4: «функционал нужен для нераспределённых
строк, чтобы суммы совпадали в КФО»). У строк агрегата отметки нет.
- **Отклонение** (DOC-272613 «Расчёт»): `deviation_amount = |amount_1 − amount_2|`;
`deviation_percent = deviation_amount / min(amount_1, amount_2) × 100`, если меньшая сторона > 0, иначе `null`.
**Порог: строго меньше 5 % — в допуске, от 5 % включительно — на проверку** (`exceeds_threshold`, DOC-272613).
**Открытый вопрос OQ-178:** реализовано (R-35) ровно 5 % — в допуске (`> 5`); аналитик на это не отвечал.
Отклонение у отдельных строк агрегата не считается (ответ п. 3).
- **Корректировка** — решение аналитика по агрегату: какие суммы и из каких статей ББ (ОФР) исключить по каждой стороне,
комментарий. Делается **только на уровне агрегата** (DOC-272613, ответ п. 2); на строку и на нераспределённую строку
корректировка не вводится. Можно корректировать и агрегат в допуске 5 %, и с отклонением больше 5 % — с обязательным
комментарием (ответ п. 8 и по ТЗ). Сохранённая корректировка не меняет суммы сторон — они всегда считаются по строкам.
## 3. Эндпоинты
| Метод и путь | Право | Запрос | Ответ | Ошибки |
|---|---|---|---|---|
| `GET /consolidations/versions/{version_id}/corrections` | VIEW | query `statement_type` (`balance` по умолчанию / `income`) | 200 `CorrectionTableOut` | 404 `NOT_FOUND` |
| `PUT /consolidations/versions/{version_id}/corrections/aggregates/{rule_id}/{company_1_id}/{company_2_id}` | EDIT | `CorrectionIn` | 200 `CorrectionAggregateOut` | 404 `NOT_FOUND` (версия, правило или агрегата нет в версии), 409 правок версии, 409 `CORRECTION_VERSION_CONFLICT`, 422 `COMMENT_REQUIRED`, 422 `EMPTY_CORRECTION`, 422 `INVALID_LINE_CODE` |
| `POST /consolidations/versions/{version_id}/corrections/allocations` | EDIT | `AllocationIn` | 200 `CorrectionTableOut` | 404 `NOT_FOUND` (версия или правило), 409 правок версии, 422 `ENTRY_NOT_IN_VERSION`, 422 `RULE_NOT_APPLICABLE`, 422 `RULE_REQUIRED`, 422 `ENTRY_ALLOCATED`, 422 `ENTRY_EXCLUDED` |
| `GET /consolidations/versions/{version_id}/corrections/export` | VIEW | query `statement_type` | файл xlsx | 404 `NOT_FOUND` |
Импорта xlsx нет — не нужен (ответ по ТЗ). Отзыва корректировки (`REVOKED`) нет — её меняют повторным сохранением (ответ п. 9).
### 3.1 `GET …/corrections` — таблица
Вся таблица одним ответом: агрегаты со строками, нераспределённые строки, итоги. Без страниц — агрегатов в версии десятки,
строк — сотни; фильтры и сортировку делает фронт.
Порядок агрегатов: по `rule_no`, затем по имени компании 1, затем компании 2. Порядок нераспределённых: по имени владельца,
затем по коду счёта.
Пустой ответ (`aggregates = []`, `unallocated = []`) — внутригрупповых строк нет; это не ошибка.
### 3.2 `PUT …/aggregates/{rule_id}/{company_1_id}/{company_2_id}` — сохранить корректировку
Создаёт корректировку агрегата или заменяет действующую целиком (все статьи обеих сторон и комментарий приходят каждый раз).
- `debit[]` — статьи стороны 1 (компания 1), `credit[]` — стороны 2 (компания 2); первая статья стороны — основная, остальные
— дочерние (хранение — `correction.parent_correction_id`, глубина 1). Пустая сторона допустима, обе пустые — 422
`EMPTY_CORRECTION`.
- `line_code` — код строки формы ББ (ОФР) этой компании; не из формы — 422 `INVALID_LINE_CODE` с `detail.line_code`.
Сумма — строка, не больше 16 цифр до запятой и двух после (`numeric(18,2)`); у ББ ≥ 0 (это уменьшение статьи), у ОФР
допустимы и отрицательные суммы (DOC-277424 §7.5). Иное — 422 массивом pydantic. У ОФР сумма — **величина исключения
в конвенции оборота**, а знак расходных строк (2120, 2210, 2220, 2330, 2350) сервер применяет сам
(`domain/kfo.py`, `apply_income_exclusions`): клиент сумму заранее не инвертирует.
- `comment` обязателен всегда, пустой после обрезки — 422 `COMMENT_REQUIRED` (DOC-272613, ответ п. 6); не длиннее
1000 символов (как комментарий смены статуса), длиннее — 422 массивом pydantic.
- Сохранение сразу применяет корректировку — `status = APPLIED`; поля `status` в теле нет (ответ п. 9). `DRAFT` есть в модели,
сервер его пока не выставляет.
- `expected_version_no` — номер редакции действующей корректировки, которую аналитик видел (`correction.version_no`).
Действующей нет — `null`. Не совпал с текущим, или `null`, а действующая есть — 409 `CORRECTION_VERSION_CONFLICT` (кто-то
сохранил раньше; фронт перечитывает таблицу).
- `line_code` — один код. У правил 1 и 2 статья стороны 2 составная (`article_2_line = "1450/1550"`, `"1310/1350"`, коды через
«/»); в корректировку пишется один из кодов.
- Корректировку можно сохранить, даже если стороны не сошлись; ответ показывает `balance` (контроль распределения). В КФО по
выбранным статьям уходит меньшая сумма (ответ п. 8). Ограничения «только при отклонении > 5 %» нет (ответ по ТЗ).
- Каждое сохранение увеличивает `version_no`; аудит `CORRECTION_CREATE` (первая) / `CORRECTION_EDIT` (следующие) с `before` /
`after`. КФО отдельной пометки не получает: до заморозки он считается на каждом чтении по текущим корректировкам, после
«Верифицирован» — только чтение (`dev/api-contract-kfo.md` §3.1). У ОФР корректировка заменяет автоисключение только
**своей** стороны агрегата.
Ответ — пересчитанный агрегат.
### 3.3 `POST …/allocations` — решение по строке
Одно действие над одной позицией: строка сводной ОСВ (`entry_ids` — как в `CorrectionEntryOut`) **и сторона** (`side`).
Сторона нужна потому, что строка с дебетовым и кредитовым сальдо одновременно даёт две позиции (§2), и решение по одной не
должно задевать другую. Хранение — `vgo_assignment` (DOC-272613 «Ручное добавление и перенос строк», §5): только ручные
решения, одно действующее на `osv_entry` и сторону в версии и типе отчётности, повторное решение обновляет запись. Позиции с
такой стороной у строки нет — 422 `ENTRY_NOT_IN_VERSION`.
| `action` | Что делает | `vgo_assignment` |
|---|---|---|
| `ASSIGN` | отнести к правилу `rule_id` или перенести в него из другого агрегата; исключённую из КФО нераспределённую строку — нельзя, 422 `ENTRY_EXCLUDED` (сначала `INCLUDE`) | `action = ASSIGN`, `rule_id` |
| `UNASSIGN` | убрать из агрегата — строка становится нераспределённой (`reason = UNASSIGNED`), автоматика её обратно не вернёт (ответ п. 4) | `action = UNASSIGNED`, `rule_id = NULL` |
| `EXCLUDE` | «Не учитывать в КФО» — **только у нераспределённой строки**; строка остаётся видна с отметкой. У строки агрегата — 422 `ENTRY_ALLOCATED` (ответ п. 4) | `is_exclude = true`; записи не было — она создаётся ради отметки, `reason` строки не меняется |
| `INCLUDE` | снять «Не учитывать в КФО»; у строки агрегата — 422 `ENTRY_ALLOCATED` | `is_exclude = false`; запись, созданная только ради отметки, удаляется |
| `RESET` | снять ручное решение — строка снова разносится автоматически | запись удаляется |
Проверки `ASSIGN` (DOC-272613, шаг 2; ответ 3(а)): правило той же версии правил и того же типа отчётности; счёт и сторона
строки входят в сторону правила с учётом исключений; пара компаний агрегата — владелец и контрагент строки (её не выбирают),
обе в периметре. Не подходит — 422 `RULE_NOT_APPLICABLE` с `detail.reason`. `ASSIGN` без `rule_id` — 422 `RULE_REQUIRED`.
Строки нет среди строк версии — 422 `ENTRY_NOT_IN_VERSION`.
После решения пересчитываются затронутые агрегаты; у агрегата с применённой корректировкой ставится
`composition_changed = true` (ответ 5). Ответ — вся таблица (решение меняет и агрегаты, и нераспределённые строки).
Аудит: реализовано существующим `audit_action.ASSIGNMENT_CHANGE` (не новым значением, как предполагалось здесь раньше) —
таблица `vgo_assignment` в DBML v5.24 (AURORA-1751); поле `side` — только для ОФР (DBML v5.27, §3.5).
### 3.4 `GET …/export` — xlsx
Таблица выбранного типа отчётности одним листом, как на экране: строка агрегата с колонками Дебет / Кредит / Контроль /
Корректировка, под ней его строки ОСВ (группировка, колонка «Правило» у них пустая), затем нераспределённые строки и итог;
суммы в рублях. Имя файла — `Корректировки_<название ГК>_<год>_<квартал>.xlsx` в `Content-Disposition` (`filename*`, ASCII
запасное `corrections.xlsx`), как в WIP API (#208). Выбор колонок и фильтры в первой версии не передаются. Аудит `EXPORT`.
### 3.5 `…/vgo-assignments/*` — разнесение строк (как реализовано) и ОФР
Фактический API разнесения (`arm-core#208`; ОФР — PR #251, DBML v5.27). Права — как в §1 (чтение `CONSOLIDATION_VIEW`,
правки `CONSOLIDATION_EDIT`).
| Метод и путь | Запрос | Ответ |
|---|---|---|
| `GET /consolidations/versions/{version_id}/vgo-assignments/distribute` | query `statement_type` (`balance` по умолчанию / `income`) | `VgoAssignmentCandidatesOut`: группы-агрегаты, ручные назначения, нераспределённые строки |
| `PUT …/vgo-assignments/rule` | `VgoAssignmentAttachRuleIn {entry_ids[], rule_id, side?}` | `VgoAssignmentOut[]` |
| `DELETE …/vgo-assignments/rule` | `VgoAssignmentDetachRuleIn {statement_type, entry_ids[], side?, reset?}` | `VgoAssignmentOut[]` (при `reset = true` — `[]`) |
| `PUT …/vgo-assignments/exclude` | `VgoAssignmentExcludeIn {statement_type, entry_ids[], is_exclude, side?}` | `VgoAssignmentOut[]` |
**`side`.** У ББ (`statement_type = balance`) `side` **не передаётся** (`NULL`): решение одно на обе позиции строки. У ОФР
(`income`) `side` **обязателен** (`"D"` или `"K"`): позиция — это «строка ОСВ + сторона», действие на оба оборота по
умолчанию не распространяется. Нарушение (ОФР без `side`, ББ с `side`) — 422 `INVALID_VGO_SIDE`. Остальные ошибки ОФР:
422 `INVALID_VGO_RULE` (правило не относится к версии правил версии консолидации), 422 `INVALID_VGO_POSITION` (позиция
или направление не подходят правилу ОФР, значения оборота нет, владелец или контрагент вне периметра), 422
`INVALID_OSV_ENTRY_IDS` (`entry_ids` не образуют полные строки свода). `side` есть и в `VgoAssignmentOut`,
`VgoAssignmentRowOut`, `VgoUnallocatedPositionOut`.
**Семантика ОФР** (DOC-277424 §3.4–3.5): `ASSIGN` — `rule_id`, `is_exclude = false`; `UNASSIGNED` — `rule_id = NULL`;
исключение назначенной позиции — `UNASSIGNED` + `is_exclude = true`; `reset` удаляет решение по позиции (она снова
разносится автоматически); `rule_id` вместе с `is_exclude = true` не хранятся. Решение по `D` не меняет решение по `K` той же
строки. **Смена разнесения ОФР не переводит применённые корректировки в `DRAFT`** (они сохраняют приоритет, DOC-277424 §3.5
п. 5); у ББ — как прежде, корректировки затронутых пар становятся `DRAFT`.
**Что приходит для ОФР в `GET …/corrections?statement_type=income` и `…/distribute?statement_type=income`:** агрегаты по
основному правилу (1, 3, 4; правила 2 и 5 — вторые стороны, собственных агрегатов нет), суммы — обороты; в строках агрегата и
в нераспределённых — `side` позиции и `company_role` (`1` или `2`: роль владельца в агрегате; для ББ `null`). Сторона может
быть без значения: `amount_present = false`, `amount`/`deviation_amount` — `null` (`NULL` не подменяется нулём,
DOC-277424 §3.1). Причина нераспределённой позиции `reason` — строка: `MULTIPLE_RULES`, `NO_RULE`, `UNASSIGNED`,
`MANUAL_REVIEW_REQUIRED`, `MISSING_AMOUNT`; у ББ — прежние `MULTIPLE_RULES` и `UNASSIGNED` (и `unassigned_reason =
PARTIAL_MANUAL_ASSIGNMENT` у строки). Правило в `rules[]` несёт `elimination_mode` (`min`, `min_or_full`, `manual`, `full`).
Корректировка ОФР вводится как у ББ (§3.2): `line_code` — статья ОФР компании, суммы со знаком; правила 6 и 7
(2340, 2350) в seed не заведены — только ручная корректировка по статье, агрегата у них нет.
## 4. Схемы
Поля без `?` есть всегда; `null` указан явно.
**`CorrectionTableOut`**
```
version_id uuid
statement_type "balance" | "income"
rules_version_no int версия правил элиминации версии консолидации
editable bool можно ли текущему пользователю править таблицу
read_only_reason null | "VERSION_NOT_EDITABLE" | "LOCK_REQUIRED" | "LOCKED_BY_OTHER"
rules[] CorrectionRuleOut правила типа отчётности (для пресетов и выбора правила)
aggregates[] CorrectionAggregateOut
unallocated[] UnallocatedEntryOut
totals CorrectionTotalsOut
```
**`CorrectionRuleOut`** `{id, rule_no, article_1_line, article_2_line, side_1 ("D"|"K"), measure ("closing"|"turnover"),
company_1_accounts[] (коды счетов стороны 1), company_2_accounts[], company_1_excluded[], company_2_excluded[]}`.
Подпись пресета фронт строит сам: «Правило N: <статья 1> — <статья 2>». Статья может быть составной — коды через «/»
(`"1450/1550"`), как в `elimination_rule`.
**`CorrectionAggregateOut`**
```
rule_id, rule_no
company_1 {id, name}
company_2 {id, name}
side_1 AggregateSideOut дебетовая сторона (компания 1)
side_2 AggregateSideOut кредитовая сторона (компания 2)
deviation_amount money
deviation_percent percent | null к меньшей стороне; null — меньшая сторона ≤ 0
exceeds_threshold bool deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой; у min_or_full — всегда false
status "AUTO" | "DISCREPANCY" | "ONE_SIDED" | "MANUAL" как EliminationTotalItemOut
eliminated_amount money автосумма агрегата по режиму правила (§8 «КФО» п. 4) — её исключает КФО, если нет корректировки; только ББ, у `income` всегда 0 (I-2)
composition_changed bool состав или суммы изменились после применённой корректировки (ответ 5; ответ п. 10 — подсвечивать)
correction CorrectionOut | null действующая корректировка (APPLIED; DRAFT — зарезервирован)
```
**`AggregateSideOut`** `{article_line (статья правила), accounts[] (коды счетов строк, вошедших в сторону; не весь перечень
правила), amount (money), entries[] (CorrectionEntryOut)}`.
**`CorrectionEntryOut`** — строка внутри агрегата:
```
entry_ids[] uuid исходные osv_entry строки свода
legal_entity_id uuid владелец строки
counterparty_id uuid
account_code string
side "D" | "K"
amount money сальдо (оборот) строки по этой стороне
decision "AUTO" | "ASSIGN" как попала в агрегат
candidate_rule_ids[] uuid другие правила, под которые строка подходит по счёту и стороне (для «Перенести в правило…»)
```
**`UnallocatedEntryOut`** — нераспределённая строка:
```
entry_ids[], owner {id, name}, counterparty {id, name}, account_code, side ("D"|"K"), amount
line_code string | null строка баланса владельца, к которой привязан счёт (ответ п. 11); null — не определена
candidate_rule_ids[] uuid правила, под которые строка подходит по счёту и стороне
reason "MULTIPLE_RULES" | "UNASSIGNED" несколько правил / убрана аналитиком
excluded bool «Не учитывать в КФО» (только у нераспределённой)
```
**`CorrectionOut`**
```
status "DRAFT" | "APPLIED" сохранение даёт APPLIED
version_no int
debit[] CorrectionLineOut статьи стороны 1, первая — основная
credit[] CorrectionLineOut статьи стороны 2, первая — основная
comment string
reason_code string | null перечень DBML (ТЗ 6.4), для ОФР
balance CorrectionBalanceOut
updated_by {id, name} | null
updated_at datetime
```
**`CorrectionLineOut`** `{line_code, amount}`.
**`CorrectionBalanceOut`** — контроль распределения (DOC-272613 «Запись значений», шаг 3):
`{debit_total, credit_total, difference (|debit_total − credit_total|), difference_percent (к меньшему, null — меньший ≤ 0),
remainder_1 (сумма стороны 1 − debit_total), remainder_2 (сумма стороны 2 − credit_total)}`.
**`CorrectionTotalsOut`** — итоги по всем агрегатам ответа (без нераспределённых): `{amount_1, amount_2, deviation_amount,
correction_debit, correction_credit}`. При фильтрах на экране фронт считает итоги видимых строк сам.
**`CorrectionIn`** `{debit[] (CorrectionLineIn), credit[] (CorrectionLineIn), comment (string), reason_code? (string|null),
expected_version_no (int|null)}`; `CorrectionLineIn {line_code, amount}`.
**`AllocationIn`** `{statement_type, entry_ids[] (≥ 1), side ("D"|"K"), action ("ASSIGN"|"UNASSIGN"|"EXCLUDE"|"INCLUDE"|"RESET"),
rule_id? (uuid, для ASSIGN)}`.
## 5. Для бэкенда (AURORA-1540)
- Модель: `correction` — как в DBML v5.24 (структура не менялась с v5.18); одна запись на сторону агрегата (основная, `parent_correction_id = NULL`) и дочерние
статьи; поля `debit_*` заполняются у записей стороны 1, `credit_*` — у стороны 2; `legal_entity_id` — компания стороны,
`counterparty_id` — другая компания агрегата. `deviation_*` записываются на момент сохранения — по ним считается
`composition_changed`.
- **Кто что делает** (`[allocation-split]`, 29.10): AURORA-1751 — разнесение строк (чистая функция) и ручные решения
(`vgo_assignment`, аудит); AURORA-1540 — таблица, корректировка, экспорт поверх функции 1751 (источник агрегатов —
`CorrectionAggregateSource`, вызывает функцию 1751).
- **Как фактически реализовано (`arm-core#208`, слито):** единой `POST …/allocations` нет — три отдельные ручки
`PUT|DELETE …/vgo-assignments/rule` (привязать/снять правило) и `PUT …/vgo-assignments/exclude` (отметка «не учитывать
в КФО»); фронт (`corrections.ts`, `allocate()`) собирает их под прежним контрактом «действие → таблица» и сам
перечитывает таблицу после вызова.
- `vgo_assignment` (DBML v5.24) — **только ручные решения**, автоматическое разнесение не хранится (DECISION-5): иначе
ручное решение не отличить от автоматического и при пересчёте после замены ОСВ оно затрётся. Поля:
`consolidation_version_id, statement_type, osv_entry_id, side (D | K, v5.27, только ОФР), rule_id (NULL), action (ASSIGN | UNASSIGNED), is_exclude
(bool), created_by/at, updated_by/at`; `ASSIGN` — с `rule_id`, `UNASSIGNED` — без; уникальность: ББ — `(версия, тип, osv_entry_id)`, ОФР — `(версия, тип, osv_entry_id, side)`; `side` обязателен у `income` и `NULL` у `balance` (§3.5). До v5.27 поля не было — решение было одно на обе позиции строки.
- Разнесение — чистая функция в `api/src/domain/` (как `elimination_packing`): строки свода × правила × ручные решения →
агрегаты и нераспределённые строки (с `line_code` и `candidate_rule_ids`). Нынешняя упаковка считает каждое правило
отдельно (TODO в `intercompany_matrix.build_pairs`) — для таблицы корректировок строка должна попадать не более чем в одно
правило.
- Правила ББ 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`, то и исключает КФО, если корректировки нет (корректировка её заменяет) —
**для ББ**. Для ОФР (I-2, отложено) таблица автосумму не считает: `eliminated_amount = 0`, `status = MANUAL`,
`exceeds_threshold` (порог 5 %) не применяется; автосуммы ОФР (правило 1 — выручка без НДС, правило 3 — вся
сторона 1, правило 4 — `min` со знаком) вычисляет и показывает только КФО (`KfoIncomeSource`, `services/kfo.py`).
- ОФР: тот же ответ с `statement_type = income`; правила 1–5 версии 1 заведены seed, агрегаты — по оборотам, позиции D/K независимы (§3.5, `05-domain-rules.md` §4 и §5 п. 2b, DOC-277424).
## 6. Пример
`GET /api/v1/consolidations/versions/7b…/corrections?statement_type=balance` (сокращено):
```json
{
"version_id": "7b1c…",
"statement_type": "balance",
"rules_version_no": 1,
"editable": true,
"read_only_reason": null,
"rules": [{"id": "r6…", "rule_no": 6, "article_1_line": "1230", "article_2_line": "1520", "side_1": "D",
"measure": "closing", "company_1_accounts": ["60.02", "62.01", "76"], "company_2_accounts": ["60.01", "62.02", "76"],
"company_1_excluded": ["76.07.5", "76.07.9"], "company_2_excluded": ["76.07.1"]}],
"aggregates": [{
"rule_id": "r6…", "rule_no": 6,
"company_1": {"id": "a…", "name": "ЭПТ"}, "company_2": {"id": "b…", "name": "ИЛС"},
"side_1": {"article_line": "1230", "accounts": ["62.01"], "amount": "1000000.00",
"entries": [{"entry_ids": ["e1…"], "legal_entity_id": "a…", "counterparty_id": "b…", "account_code": "62.01",
"side": "D", "amount": "1000000.00", "decision": "AUTO"}]},
"side_2": {"article_line": "1520", "accounts": ["60.02"], "amount": "202320.00", "entries": ["…"]},
"deviation_amount": "797680.00", "deviation_percent": "394.27", "exceeds_threshold": true,
"status": "DISCREPANCY", "eliminated_amount": "0.00", "composition_changed": false,
"correction": {"status": "APPLIED", "version_no": 1,
"debit": [{"line_code": "1230", "amount": "1000000.00"}],
"credit": [{"line_code": "1520", "amount": "202320.00"}],
"comment": "Требуется сверка с контрагентом", "reason_code": null,
"balance": {"debit_total": "1000000.00", "credit_total": "202320.00", "difference": "797680.00",
"difference_percent": "394.27", "remainder_1": "0.00", "remainder_2": "0.00"},
"updated_by": {"id": "u…", "name": "Иванов И. И."}, "updated_at": "2026-10-28T12:00:00+03:00"}
}],
"unallocated": [{"entry_ids": ["e9…"], "owner": {"id": "c…", "name": "ООО «БСК»"},
"counterparty": {"id": "d…", "name": "ПМП ООО"}, "account_code": "76.05", "line_code": "1520", "side": "K",
"amount": "142878.00", "candidate_rule_ids": ["r6…", "r7…"], "reason": "MULTIPLE_RULES", "excluded": false}],
"totals": {"amount_1": "1000000.00", "amount_2": "202320.00", "deviation_amount": "797680.00",
"correction_debit": "1000000.00", "correction_credit": "202320.00"}
}
```