From 0e95f95c712fbb9786a6bbc1051526359f8cd251 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 21 Oct 2026 14:08:08 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20change=20intercompany-matrix-?= =?UTF-8?q?presets=20+=20README=20(=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- README.md | 9 +- .../intercompany-matrix-presets/design.md | 59 +++++++++++++ .../intercompany-matrix-presets/proposal.md | 34 ++++++++ .../specs/audit-trail/spec.md | 19 +++++ .../specs/consolidated-osv/spec.md | 82 +++++++++++++++++++ .../specs/consolidations-api/spec.md | 23 ++++++ .../intercompany-matrix-presets/tasks.md | 4 + 7 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/intercompany-matrix-presets/design.md create mode 100644 openspec/changes/intercompany-matrix-presets/proposal.md create mode 100644 openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md create mode 100644 openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md create mode 100644 openspec/changes/intercompany-matrix-presets/specs/consolidations-api/spec.md create mode 100644 openspec/changes/intercompany-matrix-presets/tasks.md diff --git a/README.md b/README.md index e4e9f66..a449cdd 100644 --- a/README.md +++ b/README.md @@ -870,7 +870,14 @@ Q2–Q4 не подходят (R-14a). Привязываются только ` API: список счетов `GET /directories/accounts`, правила версии `GET …/elimination-rules`, матрица `GET …/intercompany-matrix/all` и `POST …/manual`, их XLSX-выгрузки, статичная таблица -`GET …/intercompany-matrix/result-table`. Контракт для фронта — +`GET …/intercompany-matrix/result-table`. Режим PRESET (счета и показатель из правила элиминации): +`GET …/intercompany-matrix/preset/{rule_id}`, его выгрузка `…/preset/{rule_id}/export` и строки сводной ОСВ без пары +`…/preset/{rule_id}/unpaired-osv`; в ячейке `correction_ids_debit/credit` (применённые корректировки правила). +`GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной +версии правил — открытые правила (`valid_to IS NULL`). Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`, +`ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. В шапке xlsx — строка «Показатель», +audit `EXPORT` пишет `mode` (`ALL`/`MANUAL`/`PRESET`) и `rule_id`. Открытые вопросы по пресетам — +`openspec/changes/intercompany-matrix-presets/design.md`. Контракт для фронта — `../docs/dev/frontend-api-guide.md` §5.8; требования — в существующих спецификациях `openspec/specs/{directories-api,consolidations-api,consolidated-osv,audit-trail}/spec.md`. Расчёт читает сводную ОСВ версии, результат не хранится. diff --git a/openspec/changes/intercompany-matrix-presets/design.md b/openspec/changes/intercompany-matrix-presets/design.md new file mode 100644 index 0000000..87e4f73 --- /dev/null +++ b/openspec/changes/intercompany-matrix-presets/design.md @@ -0,0 +1,59 @@ +## Context + +Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`). Шахматка — чистая функция от сводной ОСВ версии, +периметра и выбранных счетов; в БД не хранится (DECISION-5). Корректировки (`correction`) — хранимые ручные решения, +читаются, но в ядре пока никем не создаются. + +## Decisions + +1. Режим PRESET берёт правило по `rule_id`. Правило должно быть действительно для версии: при зафиксированном + `elimination_rules_version` — совпадать по `version_no`, иначе — иметь `valid_to IS NULL`; иначе 404. +2. Сторона дебета — `company_1`, если `side_1 = D`, иначе `company_2`; кредит — вторая сторона. Счета и исключения + раскрываются по иерархии (`_select_accounts`), как в MANUAL. Нет счетов хотя бы одной стороны — 422 + `ACCOUNTS_REQUIRED`; неверные `side_1`/`measure`/`company_no` — 422 `INVALID_RULE`; не передан `rule_id` — 422 + `RULE_REQUIRED`. +3. Показатель (`closing`/`turnover`) берётся из правила; ALL и MANUAL остаются на `closing`. +4. Применённые корректировки правила (`status = APPLIED`, тот же `rule_id` и версия консолидации) уменьшают + `amount_a` ячейки `(A, B)` на `debit_adjustment` корректировки (`legal_entity_id = A`, `counterparty_id = B`) и + `amount_b` на `credit_adjustment` корректировки `(B, A)`; расхождение, процент и порог пересчитываются, ID + корректировок попадают в `correction_ids_debit/credit`. +5. `unpaired-osv` строится из `one_sided`-ячеек шахматки пресета (`osv_entry_ids_*`) и возвращает строки сводной ОСВ, + содержащие эти записи. +6. `GET …/elimination-rules` отдаёт объект с основанием выбора версии; `as_of` не вводится. +7. xlsx пресета: имя `intercompany-matrix_<ГК>_<год>-<квартал>_v_preset-.xlsx`, в шапке «Режим = Пресет», + коды счетов сторон (отсортированы) и строка «Показатель» (Сальдо/Оборот) для всех режимов. + +## Known issues / вопросы владельцу + +Спека описывает фактическое поведение PR; пункты ниже не закреплены как окончательные и ждут решения автора и владельца. + +1. При пустом `consolidation_version.elimination_rules_version` берутся текущие открытые правила. Это расходится с + DBML («фиксируется при первом расчёте, замораживается при VERIFIED») и критерием AURORA-1536; нужно решение + владельца, в том числе для версий в статусе `VERIFIED`. +2. «Актуальные» правила — `valid_to IS NULL` на сегодня, а не на отчётную дату консолидации. +3. Если открытыми остаются несколько `version_no`, список правил даёт дубли `rule_no`. +4. Семантика корректировки: `(rule, A, B)` дебетовой частью правит ячейку `(A, B)`, кредитовой — ячейку `(B, A)`; + `debit_account_id`, `credit_account_id` и `parent_correction_id` не учитываются; корректировки в ядре пока никто + не создаёт. +5. `unpaired-osv` построена на `one_sided` по наличию строк — это дефект AURORA-1651, и «без пары» здесь не равно + «без пары» по §5 и AURORA-1536. +6. Пресет здесь — правило Прил. 3, а §6 описывает пресеты как наборы счетов; PRESET по ОФР на оборотах + (`measure = turnover`) не подтверждён §5/§6. +7. Имя файла содержит UUID правила, шапка xlsx не содержит номер правила и версию; audit не пишет номер/версию правил + и ID применённых корректировок. + +## Decisions made without discussion + +- 2026-10-21. Change пишет ревьюер в ветке автора по решению владельца. Обоснование: автор не оформила OpenSpec. Цена + ошибки: автор переписывает change. +- 2026-10-21. Спека описывает фактическое поведение PR; спорное вынесено в Known issues. Обоснование: домен решает + автор и владелец, не ревьюер. Цена ошибки: после решения владельца спека меняется дельтой. +- 2026-10-21. Поле `as_of` удалено из `EliminationRulesOut` как мёртвое: после правок по ревью всегда `None`, в main + его не было, потребителей нет. Цена ошибки: если фронт ждал `as_of`, поле нужно вернуть. +- 2026-10-21. Интеграционный тест `test_preset_uses_current_rules_when_version_unfixed` приведён к фактическому + поведению (`version_no is None`, `is_version_fixed is False`), а не к старому ожиданию `version_no == rule.version_no`; + логика вычисления `version_no` не добавлялась. Цена ошибки: если автор решит возвращать номер версии, тест вернуть. +- 2026-10-21. mypy: `measure` аннотирован `Measure` без `cast`; слишком длинная f-строка имени файла свода ОСВ + (`consolidated_osv.py`) разбита на две без изменения результата (ruff E501). Цена ошибки: нулевая. +- 2026-10-21. В MODIFIED-блоке требования про правила элиминации и аудит экспорта приведён полный текст; требование + «XLSX шахматки» (ALL/MANUAL) не менялось, поведение PRESET описано отдельным ADDED-требованием. diff --git a/openspec/changes/intercompany-matrix-presets/proposal.md b/openspec/changes/intercompany-matrix-presets/proposal.md new file mode 100644 index 0000000..cb59d0e --- /dev/null +++ b/openspec/changes/intercompany-matrix-presets/proposal.md @@ -0,0 +1,34 @@ +## Why + +Шахматка ВГО (ТЗ §6) считалась только по всем счетам (ALL) или по счетам, выбранным вручную (MANUAL). Аналитику нужен +режим «пресет»: счета и показатель берутся из правила элиминации (Прил. 3), с учётом применённых корректировок и со +списком строк сводной ОСВ без пары. Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`, автор — Потапова). +Требования оформлены как дельты этого change'а при ревью (решение владельца 21.10.2026); спорное поведение вынесено в +`design.md`, `## Known issues`, и в спеке не закреплено как окончательное. + +## What Changes + +- Режим PRESET шахматки: `GET …/intercompany-matrix/preset/{rule_id}`, выгрузка `…/preset/{rule_id}/export`, список + строк без пары `…/preset/{rule_id}/unpaired-osv`. +- Показатель пресета (`closing` / `turnover`) берётся из правила; в ячейке появляются `correction_ids_debit/credit`, + суммы уменьшаются на применённые корректировки правила. +- `GET …/elimination-rules` возвращает объект `{version_no, is_version_fixed, rules}` вместо массива; у правила есть + `id`. Для версии без зафиксированного `elimination_rules_version` отдаются открытые правила (`valid_to IS NULL`). +- xlsx: для PRESET имя файла с `_preset-`, в шапке строка «Показатель»; audit `EXPORT` пишет `mode` + `ALL`/`MANUAL`/`PRESET` и `rule_id`. + +## Capabilities + +### New Capabilities +Нет. + +### Modified Capabilities +- `consolidations-api`: ответ правил элиминации версии. +- `consolidated-osv`: режим PRESET шахматки, корректировки, строки без пары, xlsx пресета. +- `audit-trail`: аудит экспорта шахматки (режим PRESET). + +## Impact + +Только `arm-core` (API, сервисы, схемы), таблицы и DBML не меняются. Ломающее изменение контракта +`GET …/elimination-rules` (массив → объект); поле `as_of` в ответ не входит. Потребителей старого формата в этом репозитории не проверялось — проверить при слиянии. Non-goals: фиксация версии правил, расчёт «актуальных» правил на отчётную дату, создание корректировок, +исправление `one_sided` (AURORA-1651). diff --git a/openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md b/openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md new file mode 100644 index 0000000..838a8d1 --- /dev/null +++ b/openspec/changes/intercompany-matrix-presets/specs/audit-trail/spec.md @@ -0,0 +1,19 @@ +## MODIFIED Requirements + +### Requirement: Аудит экспорта шахматки +Каждый успешный `GET …/intercompany-matrix/all/export`, `POST …/intercompany-matrix/manual/export` и +`GET …/intercompany-matrix/preset/{rule_id}/export` +SHALL добавлять отдельное неизменяемое событие `audit_event` с `action = EXPORT`, `section = EXPORT`, +`object_type = consolidation_version`, `object_id = consolidation_version_id`, `user_id` инициатора, +`before_value = null` и `after_value = {report: "intercompany_matrix", mode, debit_account_ids, +credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть дополнительно `rule_id`. `mode` — `ALL`, +`MANUAL` или `PRESET`, массивы счетов — ID из запроса (в ALL и PRESET пустые), `cells` — число ячеек. Выгрузка +сводной ОСВ продолжает писать собственное событие `EXPORT`. + +#### Scenario: Две выгрузки +- **WHEN** пользователь дважды выгружает ту же шахматку +- **THEN** в `audit_event` две разные записи `EXPORT`, каждая со своим payload и числом ячеек + +#### Scenario: Выгрузка пресета +- **WHEN** пользователь выгружает `GET …/intercompany-matrix/preset/{rule_id}/export` +- **THEN** событие `EXPORT` содержит `mode = PRESET`, `rule_id` правила, пустые массивы счетов и число ячеек diff --git a/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md b/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md new file mode 100644 index 0000000..4e986b8 --- /dev/null +++ b/openspec/changes/intercompany-matrix-presets/specs/consolidated-osv/spec.md @@ -0,0 +1,82 @@ +## ADDED Requirements + +### Requirement: Шахматка по правилу-пресету +`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}` +(`CONSOLIDATION_VIEW`, policy) SHALL рассчитывать шахматку в режиме PRESET с тем же ответом, что ALL и MANUAL. +Счета дебета и кредита SHALL браться из правила элиминации `rule_id`: дебет — сторона `company_1`, если +`side_1 = D`, иначе `company_2`; кредит — вторая сторона; исключения (`excluded`) вычитаются, выбранные счета +раскрываются по иерархии и фильтруются по виду, как в MANUAL. Показатель SHALL браться из `measure` правила: +`closing` — конечное сальдо (`closing_debit`/`closing_credit`), `turnover` — обороты (`turnover_debit`/`turnover_credit`). +Правило SHALL быть действительным для версии консолидации: при заданном `elimination_rules_version` — с тем же +`version_no`, иначе — без `valid_to`; несуществующее или недействительное правило SHALL давать 404. Ошибки 422: +`RULE_REQUIRED` — правило не выбрано, `INVALID_RULE` — неверные `side_1`, `measure` или сторона счёта правила, +`ACCOUNTS_REQUIRED` — у одной из сторон нет счетов. Невидимая и несуществующая версия SHALL отвечать 404 `NOT_FOUND`. + +#### Scenario: Пресет по сальдо +- **WHEN** правило с `side_1 = D`, `measure = closing` содержит дебетовые счета на стороне 1 и кредитовые на стороне 2 +- **THEN** `amount_a` пары — сумма `closing_debit` по счетам стороны 1, `amount_b` — сумма `closing_credit` по счетам стороны 2 + +#### Scenario: Пресет по оборотам +- **WHEN** у правила `measure = turnover` +- **THEN** суммы считаются по `turnover_debit`/`turnover_credit` + +#### Scenario: Правило другой версии +- **WHEN** у версии `elimination_rules_version = 2`, а `rule_id` принадлежит правилу версии 1 +- **THEN** 404 для расчёта и выгрузки + +#### Scenario: Неизвестное правило +- **WHEN** `rule_id` не существует +- **THEN** 404 + +### Requirement: Корректировки в шахматке пресета +В режиме PRESET расчёт SHALL учитывать корректировки `correction` со статусом `APPLIED`, у которых +`consolidation_version_id` равен версии, а `rule_id` — правилу пресета. Корректировка с `legal_entity_id = A` и +`counterparty_id = B` SHALL уменьшать `amount_a` ячейки `(A, B)` на `debit_adjustment` и `amount_b` ячейки `(B, A)` на +`credit_adjustment`. Затронутые ячейки SHALL получать пересчитанные `variance_amount`, `variance_percent`, +`exceeds_threshold` и ID корректировок в `correction_ids_debit`/`correction_ids_credit`; у остальных ячеек эти массивы +пусты (поля есть в ответе всех режимов). Корректировки без `legal_entity_id` или `counterparty_id` SHALL игнорироваться. + +#### Scenario: Корректировка по дебету +- **WHEN** есть применённая корректировка правила `(A, B)` с `debit_adjustment = 10` +- **THEN** у ячейки `(A, B)` `amount_a` меньше на 10, расхождение пересчитано, `correction_ids_debit` содержит её ID + +#### Scenario: Корректировка по кредиту +- **WHEN** есть применённая корректировка правила `(A, B)` с `credit_adjustment = 5` +- **THEN** у ячейки `(B, A)` `amount_b` меньше на 5, `correction_ids_credit` содержит её ID + +#### Scenario: Корректировка чужого правила или не применённая +- **WHEN** корректировка относится к другому правилу либо имеет статус не `APPLIED` +- **THEN** суммы ячеек не меняются, `correction_ids_*` пусты + +### Requirement: Строки сводной ОСВ без пары по пресету +`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/unpaired-osv` +(`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{items, total}`: все строки сводной ОСВ версии, чьи записи ОСВ +входят в `osv_entry_ids_debit/credit` ячеек шахматки пресета с `one_sided = true`. Формат строки — как на странице +сводной ОСВ, без пагинации; `total` — число элементов. Ошибки — как у расчёта пресета. + +#### Scenario: Есть односторонняя ячейка +- **WHEN** в шахматке пресета у ячейки `(A, B)` `one_sided = true` +- **THEN** `items` содержит строки сводной ОСВ с записями этой ячейки, `total` равен их числу + +#### Scenario: Все пары двусторонние +- **WHEN** у шахматки пресета нет односторонних ячеек +- **THEN** `items = []`, `total = 0` + +#### Scenario: Неизвестное правило +- **WHEN** `rule_id` не существует +- **THEN** 404 + +### Requirement: XLSX шахматки пресета +`GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт +пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v_preset-.xlsx`. Шапка SHALL +содержать режим «Пресет», коды счетов дебета и кредита правила и строку «Показатель» со значением «Сальдо» для +`closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение +«Сальдо»). + +#### Scenario: Выгрузка по оборотам +- **WHEN** пользователь выгружает пресет с `measure = turnover` +- **THEN** шапка содержит «Режим — Пресет» и «Показатель — Оборот», имя файла заканчивается на `_preset-.xlsx` + +#### Scenario: Правило не найдено +- **WHEN** `rule_id` не существует +- **THEN** 404, файл не отдаётся, событие аудита не пишется diff --git a/openspec/changes/intercompany-matrix-presets/specs/consolidations-api/spec.md b/openspec/changes/intercompany-matrix-presets/specs/consolidations-api/spec.md new file mode 100644 index 0000000..1217e97 --- /dev/null +++ b/openspec/changes/intercompany-matrix-presets/specs/consolidations-api/spec.md @@ -0,0 +1,23 @@ +## MODIFIED Requirements + +### Requirement: Правила элиминации версии +`GET /api/v1/consolidations/versions/{version_id}/elimination-rules` (`CONSOLIDATION_VIEW`, policy по ГК) +SHALL возвращать объект `{version_no, is_version_fixed, rules}`. Если у версии задан +`consolidation_version.elimination_rules_version`, `version_no` SHALL равняться ему, `is_version_fixed = true`, а +`rules` — правилами этой версии. Если он не задан, `version_no = null`, `is_version_fixed = false`, а `rules` — +открытые правила (`valid_to IS NULL`); при их отсутствии `rules = []`. Каждое правило SHALL содержать `id`, `rule_no`, +`version_no`, `statement_type`, `side_1` (`D`/`K`), `measure` (`closing`/`turnover`), `company_1`, `company_2`; +каждая сторона SHALL содержать `accounts: UUID[]` и `excluded: UUID[]` до раскрытия иерархии счетов. +Невидимая и несуществующая версия SHALL отвечать одинаковым 404 `NOT_FOUND`. + +#### Scenario: Правило с исключением +- **WHEN** у правила первая сторона включает счёт 76 и исключает 76.07.5 +- **THEN** ID 76 находится в `company_1.accounts`, ID 76.07.5 — в `company_1.excluded` + +#### Scenario: Версия правил зафиксирована +- **WHEN** у версии консолидации `elimination_rules_version = 1` +- **THEN** ответ содержит `is_version_fixed = true`, `version_no = 1` и только правила с `version_no = 1` + +#### Scenario: Версия правил не зафиксирована +- **WHEN** у версии консолидации `elimination_rules_version` пуст +- **THEN** ответ содержит `is_version_fixed = false`, `version_no = null` и открытые правила с `valid_to IS NULL` diff --git a/openspec/changes/intercompany-matrix-presets/tasks.md b/openspec/changes/intercompany-matrix-presets/tasks.md new file mode 100644 index 0000000..7428843 --- /dev/null +++ b/openspec/changes/intercompany-matrix-presets/tasks.md @@ -0,0 +1,4 @@ +## 1. Реализация + +Реализация выполнена автором в PR RSHB/arm-core#141; задач плана нет. Доработки по замечаниям ревью — см. +`design.md`, `## Known issues`.