docs(openspec): change intercompany-matrix-presets + README (ревью #141)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-21 14:08:08 +04:00
parent 367a11ed38
commit 0e95f95c71
7 changed files with 229 additions and 1 deletions

View File

@ -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`.
Расчёт читает сводную ОСВ версии, результат не хранится.

View File

@ -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<n>_preset-<rule_id>.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-требованием.

View File

@ -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-<rule_id>`, в шапке строка «Показатель»; 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).

View File

@ -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` правила, пустые массивы счетов и число ячеек

View File

@ -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<n>_preset-<rule_id>.xlsx`. Шапка SHALL
содержать режим «Пресет», коды счетов дебета и кредита правила и строку «Показатель» со значением «Сальдо» для
`closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение
«Сальдо»).
#### Scenario: Выгрузка по оборотам
- **WHEN** пользователь выгружает пресет с `measure = turnover`
- **THEN** шапка содержит «Режим — Пресет» и «Показатель — Оборот», имя файла заканчивается на `_preset-<rule_id>.xlsx`
#### Scenario: Правило не найдено
- **WHEN** `rule_id` не существует
- **THEN** 404, файл не отдаётся, событие аудита не пишется

View File

@ -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`

View File

@ -0,0 +1,4 @@
## 1. Реализация
Реализация выполнена автором в PR RSHB/arm-core#141; задач плана нет. Доработки по замечаниям ревью — см.
`design.md`, `## Known issues`.