Markdown и OpenAPI для AURORA-1540 (бэкенд) и AURORA-1541 (фронт): таблица агрегатов и нераспределённых строк, сохранение и отзыв корректировки агрегата, решения по строке (vgo_assignment), экспорт. Основа — аналитика Лысенко (DOC-272613, ответы 1–7, DOC-274230) и дизайн 23.10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
26 KiB
Контракт API таблицы корректировок (AURORA-1540 / AURORA-1541)
Черновик контракта для бэкенда (AURORA-1540) и фронта (AURORA-1541, arm-core #206). Машиночитаемая копия —
dev/api-contract-corrections.openapi.yaml; при расхождении двух файлов правится тот, что ошибся, и оба держатся одинаковыми.
Источники, в порядке приоритета:
- Аналитика Лысенко: DOC-272613 «Корректировка данных агрегатом», ответы на вопросы 1–7 (комментарий от 27.10.2026 в AURORA-1395), DOC-274230 «Правила пар счетов».
- Дизайн таблицы корректировок от 23.10.2026 (вложение к DOC-272613).
- ТЗ 6.8, Прил. 9 — только там, где аналитика молчит.
Открытые вопросы к аналитику — комментарий [design-23-10-check] в AURORA-1395 (12 пунктов + 3 по ТЗ) и в AURORA-1396
(ОФР). Места контракта, которые от них зависят, помечены (ждёт п. N).
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"иside = "K". -
Агрегат — правило × компания 1 × компания 2 (× тип отчётности, он следует из правила). Отдельно не хранится, считается при каждом запросе (DOC-272613 §3). Ключ агрегата —
(rule_id, company_1_id, company_2_id). -
Разнесение (DOC-272613 «Распределение строк по правилам», ответы 1(б), 2(а)):
- ручное решение по строке (
vgo_assignment) важнее автоматики; - строка подходит под одно правило — в это правило, даже без встречной стороны;
- под несколько правил и у второй компании есть строки по счетам другой стороны ровно одного из них — в это правило;
- пара есть в нескольких правилах — в правило с меньшим номером Прил. 3 (ответ 2(а));
- иначе — нераспределённая строка.
Каждая строка учитывается не более чем в одном агрегате своего типа отчётности.
- ручное решение по строке (
-
Сторона агрегата. Сторона 1 — строки компании 1 по счетам стороны 1 правила, сторона 2 — строки компании 2 по счетам стороны 2. Сумма стороны — сумма строк, не отмеченных «не включать в КФО».
-
Отклонение (DOC-272613 «Расчёт»):
deviation_amount = |amount_1 − amount_2|;deviation_percent = deviation_amount / min(amount_1, amount_2) × 100, если меньшая сторона > 0, иначеnull. Порог: строго меньше 5 % — в допуске, от 5 % включительно — на проверку (exceeds_threshold). Внимание: нынешнийcalculate_rule_totalsсчитает> 5; по аналитике нужно≥ 5. -
Корректировка — решение аналитика по агрегату: какие суммы и из каких статей ББ (ОФР) исключить по каждой стороне, комментарий. Делается только на уровне агрегата (DOC-272613); на строку и на нераспределённую строку корректировка не вводится (ждёт п. 2). Сохранённая корректировка не меняет суммы сторон — они всегда считаются по строкам.
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/aggregates/{rule_id}/{company_1_id}/{company_2_id}/revoke |
EDIT | CorrectionRevokeIn {comment} |
200 CorrectionAggregateOut |
404 NOT_FOUND, 409 правок версии, 409 CORRECTION_NOT_APPLIED, 422 COMMENT_REQUIRED |
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 |
GET /consolidations/versions/{version_id}/corrections/export |
VIEW | query statement_type |
файл xlsx | 404 NOT_FOUND |
Импорта xlsx в контракте нет — ждёт ответа по ТЗ 6.8 (третий пункт по ТЗ в [design-23-10-check]).
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). Пустая сторона допустима, обе пустые — 422EMPTY_CORRECTION.line_code— код строки формы ББ (ОФР) этой компании; не из формы — 422INVALID_LINE_CODEсdetail.line_code. Сумма — строка ≥ 0 с не более чем двумя знаками; это уменьшение статьи.commentобязателен, пустой после обрезки — 422COMMENT_REQUIRED(DOC-272613, ТЗ 6.8) (ждёт п. 6); не длиннее 1000 символов (как комментарий смены статуса), длиннее — 422 массивом pydantic. То же уrevoke.status—APPLIED(по умолчанию) илиDRAFT. В дизайне одна кнопка «Сохранить корректировки», фронт шлётAPPLIED(ждёт п. 9).expected_version_no— номер редакции действующей корректировки, которую аналитик видел (correction.version_no). Действующей (DRAFT/APPLIED) нет — в том числе после отзыва —null. Не совпал с текущим, илиnull, а действующая есть — 409CORRECTION_VERSION_CONFLICT(кто-то сохранил раньше; фронт перечитывает таблицу). Новая корректировка после отзыва — новые записиcorrection; отозванные остаются в истории.line_code— один код. У правил 1 и 2 статья стороны 2 составная (article_2_line = "1450/1550","1310/1350", коды через «/»); в корректировку пишется один из кодов.- Корректировку можно сохранить, даже если стороны не сошлись; ответ показывает
balance(контроль распределения) (ждёт п. 8). Ограничения «только при отклонении > 5 %» нет — в аналитике его нет (вопрос по ТЗ открыт). - Каждое сохранение увеличивает
version_no; аудитCORRECTION_CREATE(первая) /CORRECTION_EDIT(следующие) сbefore/after; результат КФО помечается устаревшим (AURORA-1417).
Ответ — пересчитанный агрегат.
3.3 POST …/revoke — отозвать корректировку
Действующая корректировка агрегата получает статус REVOKED и остаётся в истории; комментарий обязателен. Действующей
нет — 409 CORRECTION_NOT_APPLIED. Аудит CORRECTION_REVOKE. (ждёт п. 9 — как отзывать в интерфейсе)
3.4 POST …/allocations — решение по строке
Одно действие над одной строкой сводной ОСВ (entry_ids — как в CorrectionEntryOut). Хранение — vgo_assignment
(DOC-272613 «Ручное добавление и перенос строк»), одно действующее решение на osv_entry в версии и типе отчётности,
повторное решение обновляет запись.
action |
Что делает | vgo_assignment |
|---|---|---|
ASSIGN |
отнести к правилу rule_id или перенести в него из другого агрегата |
action = ASSIGN, rule_id |
UNASSIGN |
убрать из агрегата; автоматика её обратно не вернёт | action = UNASSIGNED, rule_id = NULL |
EXCLUDE |
«Не включать в КФО»: строка не входит в суммы сторон и элиминацию, остаётся видна с отметкой | action = EXCLUDE, прежний rule_id сохраняется |
INCLUDE |
снять «Не включать в КФО»: вернуть прежнее ручное отнесение, если оно было, иначе — автоматика | action = ASSIGN с прежним rule_id или запись удаляется |
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 DBML v5.23 подходящего действия нет — нужно новое (например, VGO_ASSIGNMENT_CHANGE) вместе с
таблицей vgo_assignment в новой версии DBML.
3.5 GET …/export — xlsx
Таблица выбранного типа отчётности, как на экране: агрегаты с колонками Дебет / Кредит / Контроль / Корректировка, затем
нераспределённые строки; суммы в рублях. Выбор колонок и фильтры в первой версии не передаются. Аудит EXPORT.
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, или одна сторона пуста при ненулевой другой
status "AUTO" | "DISCREPANCY" | "ONE_SIDED" | "MANUAL" как EliminationTotalItemOut
eliminated_amount money автоматическая сумма элиминации агрегата (§5), справочно
composition_changed bool состав или суммы изменились после применённой корректировки (ответ 5)
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" как попала в агрегат
excluded bool «Не включать в КФО»; в сумму стороны не входит
candidate_rule_ids[] uuid другие правила, под которые строка подходит по счёту и стороне (для «Перенести в правило…»)
UnallocatedEntryOut — нераспределённая строка:
entry_ids[], owner {id, name}, counterparty {id, name}, account_code, side ("D"|"K"), amount
candidate_rule_ids[] uuid правила, под которые строка подходит по счёту и стороне
reason "MULTIPLE_RULES" | "UNASSIGNED" несколько правил / убрана аналитиком
excluded bool
Строка баланса владельца у нераспределённой строки не передаётся (DOC-272613 §4.3) (ждёт п. 11).
CorrectionOut
status "DRAFT" | "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), status? ("APPLIED" по умолчанию | "DRAFT"), expected_version_no (int|null)}; CorrectionLineIn {line_code, amount}.
CorrectionRevokeIn {comment}.
AllocationIn {statement_type, entry_ids[] (≥ 1), action ("ASSIGN"|"UNASSIGN"|"EXCLUDE"|"INCLUDE"|"RESET"), rule_id? (uuid, для ASSIGN)}.
5. Для бэкенда (AURORA-1540)
- Модель:
correction— как в DBML v5.23; одна запись на сторону агрегата (основная,parent_correction_id = NULL) и дочерние статьи; поляdebit_*заполняются у записей стороны 1,credit_*— у стороны 2;legal_entity_id— компания стороны,counterparty_id— другая компания агрегата.deviation_*записываются на момент сохранения — по ним считаетсяcomposition_changed. vgo_assignment— новая таблица (DOC-272613):consolidation_version_id, statement_type, osv_entry_id, rule_id (NULL), action (ASSIGN | UNASSIGNED | EXCLUDE), created_by/at, updated_by/at, уникальность(consolidation_version_id, statement_type, osv_entry_id). Нужна новая версия DBML иaudit_actionдля решений по строкам.- Разнесение — чистая функция в
api/src/domain/(какelimination_packing), без хранения агрегатов. Нынешняя упаковка считает каждое правило отдельно (TODO вintercompany_matrix.build_pairs) — для таблицы корректировок строка должна попадать не более чем в одно правило. - Правила ББ 1 и 2 (DOC-274230) имеют свой расчёт и не наследуют min; что идёт в
eliminated_amountпо ним — по DOC-274230. - ОФР: тот же ответ с
statement_type = income(ждёт ответа в AURORA-1396); до него сервер может отдавать пустыеaggregatesиunallocatedдляincome.
6. Пример
GET /api/v1/consolidations/versions/7b…/corrections?statement_type=balance (сокращено):
{
"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", "excluded": false}]},
"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", "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"}
}