docs: change intercompany-matrix-one-sided и README — one_sided по суммам, ALL без забалансовых (AURORA-1651)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-22 23:45:57 +04:00
parent 9a30b45c17
commit 0db60b7315
6 changed files with 149 additions and 0 deletions

View File

@ -873,6 +873,8 @@ API: список счетов `GET /directories/accounts`, правила ве
`GET …/intercompany-matrix/result-table` и её XLSX `GET …/intercompany-matrix/result-table/export`
(аудит `EXPORT`, `report = "intercompany_result_table"`). Режим PRESET (счета и показатель из правила элиминации):
`GET …/intercompany-matrix/preset/{rule_id}` и его выгрузка `…/preset/{rule_id}/export`.
Режим ALL — только балансовые счета (без 001–012), MANUAL принимает и забалансовые. `one_sided` — ровно одна сумма
пары нулевая; пара подсвечивается (и в xlsx) при `exceeds_threshold` или `one_sided` (AURORA-1651).
`GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной
версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`,
`ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. Имя XLSX пресета оканчивается на

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-22

View File

@ -0,0 +1,31 @@
## Context
PR RSHB/arm-core#165, задача AURORA-1651 (технический долг ревью PR #114). Шахматка — чистая функция от сводной ОСВ
версии (DECISION-5), контракт ответа не меняется: меняются смысл `one_sided`, состав счетов режима ALL и заливка xlsx.
## Decisions
1. `one_sided = (amount_a == 0) != (amount_b == 0)`. Формула из описания задачи (`min(amount_a, amount_b) == 0 and
variance_amount > 0`) на неотрицательных суммах даёт то же, но на отрицательном сальдо (возможно по §1) пропускает
пару: −50 против 0 → `min = −50`. Знак ненулевой суммы не важен.
2. Подсветка в API и в xlsx — `exceeds_threshold or one_sided`. Поле `exceeds_threshold` сохраняет смысл «процент > 5»;
экран шахматки красит пару по тому же правилу (PR #158, change `intercompany-matrix-ui`).
3. Забалансовые счета (`is_off_balance = true`) исключаются только в ALL; MANUAL выбирает их явно, это не ошибка.
4. Фильтры `GET /directories/accounts` (`kind[]`, `row_type`, `is_off_balance`) из первой редакции PR убраны: в задаче их
нет, потребителей нет, при фильтре по `kind` в ответе оставались `parent_id` на строки вне ответа.
## Decisions made without discussion
- 2026-10-22. Формула `one_sided` через «ровно одна сумма нулевая» вместо формулы из описания задачи. Обоснование —
отрицательное сальдо (решение 1). Цена ошибки: если аналитик решит, что отрицательная сторона — не «пара без
встречной стороны», меняется одна строка и тест.
- 2026-10-22. Влив `main` с #141 и разрешение конфликтов выполнил ревьюер (владелец: «доведём задачу до ума, потом
сольём»). В ALL сохранены и фильтр забалансовых, и `MatrixSelection(..., "closing")` из #141; тесты #141 на старый
смысл `one_sided` переписаны. Цена ошибки: низкая, расчёт PRESET не затронут (свои счета правила).
## Решения владельца (2026-10-22)
- Экран красит и по `one_sided` (а не бэкенд выставляет `exceeds_threshold`).
- Фильтр `/directories/accounts` убрать из PR.
- Счёт `000` «Вспомогательный» не трогать, вопрос — в `07-open-questions.md`.
- Процент при отрицательной меньшей стороне — вопрос в `07-open-questions.md`, в этом PR не меняется.

View File

@ -0,0 +1,34 @@
## Why
Технический долг повторного ревью PR #114 (AURORA-1537), задача AURORA-1651. Признак `one_sided` считался по наличию
строк ОСВ, а не по суммам: пара «100 млн против строки с нулевым сальдо» выглядела сошедшейся (`one_sided = false`,
`exceeds_threshold = false`), а «0.00 против 0.00» с одной строкой — подсвечивалась ложно. Книга xlsx заливала пару только
по порогу. Режим «все счета» брал забалансовые счета 001–012: например, 009 «Обеспечения выданные» на компанию группы
давал ложное расхождение (решение владельца 16.10.2026).
Реализация — PR RSHB/arm-core#165 (ветка `AURORA-1651`, автор — Потапова), доведена ревьюером по решению владельца
22.10.2026.
## What Changes
- `one_sided` = ровно одна из сумм пары нулевая (знак не важен: сальдо в ОСВ бывает отрицательным).
- XLSX шахматки заливает пару, если `exceeds_threshold` или `one_sided`; в шапке режима ALL — «Все балансовые счета».
- Режим ALL — только балансовые счета (`is_off_balance = false`); MANUAL принимает любые, включая забалансовые.
- Правила — `../docs/context/05-domain-rules.md` §6 (docs-PR вместе с этим change), открытые вопросы — OQ про счёт `000`
и процент при отрицательной меньшей стороне.
## Non-goals
- Подсветка односторонней пары на экране шахматки — фронт, PR #158 (change `intercompany-matrix-ui`).
- Фильтры `GET /directories/accounts` (были в первой редакции PR, убраны решением владельца 22.10.2026).
- Процент расхождения при отрицательной меньшей стороне и исключение счёта `000` из ALL — открытые вопросы.
## Capabilities
### Modified Capabilities
- `consolidated-osv`: требования «Шахматка по сводной ОСВ версии» и «XLSX шахматки».
## Impact
- `api/src/services/intercompany_matrix.py`, `api/src/services/intercompany_matrix_export.py`; тесты шахматки.
- Контракт API не меняется (поля те же), меняется смысл `one_sided` и состав счетов режима ALL.

View File

@ -0,0 +1,72 @@
## MODIFIED Requirements
### Requirement: Шахматка по сводной ОСВ версии
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/all` и
`POST /api/v1/consolidations/versions/{version_id}/intercompany-matrix/manual`
(`CONSOLIDATION_VIEW`, policy) SHALL рассчитывать матрицу без сохранения в БД, из строк сводной ОСВ версии
и её периметра. ALL включает балансовые счета (`is_off_balance = false`) вида `A`/`AP` для дебета и `P`/`AP` для
кредита; забалансовые счета (001–012) в ALL MUST NOT входить. MANUAL принимает обязательные непустые
`debit_account_ids: UUID[]` и `credit_account_ids: UUID[]`, раскрывает выбранные счета по иерархии и оставляет
подходящий вид; забалансовые счета в MANUAL допускаются. Неизвестный счёт SHALL давать 422 `ACCOUNT_NOT_FOUND`, неверный
вид — 422 `ACCOUNT_KIND_MISMATCH`; пустой массив — 422 валидации тела. Невидимая и несуществующая версия SHALL
отвечать одинаковым 404 `NOT_FOUND`.
Ответ SHALL содержать `companies[] {id, name}` для всего периметра и `cells[]` для каждой упорядоченной пары
разных компаний: `company_a_id`, `company_b_id`, `amount_a`, `amount_b`, `variance_amount`, `variance_percent`,
`exceeds_threshold`, `one_sided`, `skipped_osv_entry_count`, `osv_entry_ids_debit`, `osv_entry_ids_credit`.
Для пары `(A, B)` `amount_a` — сумма `closing_debit` строк A с контрагентом B по дебетовым счетам,
`amount_b` — сумма `closing_credit` строк B с контрагентом A по кредитовым. Расхождение — модуль разности;
процент — расхождение / меньшую сумму × 100 или null при нуле. `exceeds_threshold` SHALL быть true при точном
проценте > 5, до округления; суммы и процент в JSON SHALL быть десятичными строками с двумя знаками после
запятой (`ROUND_HALF_UP`). `one_sided` (пара без встречной стороны) SHALL быть true тогда и только тогда, когда
ровно одна из сумм `amount_a`, `amount_b` равна нулю (знак ненулевой суммы не важен); наличие строк ОСВ с нулевым
сальдо на признак не влияет. Пара подсвечивается, если `exceeds_threshold` или `one_sided`. `skipped_osv_entry_count`
SHALL считать исходные строки ОСВ пары с `account_id = null` на обеих сторонах; они не входят в суммы. Массивы
`osv_entry_ids_debit/credit` SHALL содержать ID исходных записей соответствующей стороны.
#### Scenario: Пустой периметр и отсутствие ОСВ
- **WHEN** у версии нет членов периметра
- **THEN** `companies = []`, `cells = []`; если компании есть, но ОСВ по выбранным счетам нет, ячейки присутствуют с нулевыми суммами
#### Scenario: Только одна сторона пары
- **WHEN** у A есть строка ОСВ с контрагентом B и ненулевым сальдо, а у B встречной строки нет
- **THEN** для `(A, B)` `one_sided = true`, `variance_percent = null`, соответствующий массив `osv_entry_ids_*` пуст
#### Scenario: Встречная строка с нулевым сальдо
- **WHEN** у A по B сальдо 100 000 000, а у B по A есть строка ОСВ с нулевым сальдо (одни обороты)
- **THEN** `one_sided = true`, `exceeds_threshold = false`, пара подсвечивается
#### Scenario: Обе стороны нулевые
- **WHEN** у A и у B есть строки ОСВ друг на друга, но обе суммы равны 0
- **THEN** `one_sided = false`, `exceeds_threshold = false`, пара не подсвечивается
#### Scenario: Отрицательное сальдо
- **WHEN** у A по B сальдо −50, у B по A — 0
- **THEN** `one_sided = true`
#### Scenario: Забалансовые счета
- **WHEN** у A по B есть сальдо на счёте 001 (`is_off_balance = true`)
- **THEN** в режиме ALL оно в суммы не входит; в MANUAL с выбранным счётом 001 — входит
#### Scenario: Счёт не сопоставлен
- **WHEN** у пары есть строка свода с `account_id = null` и двумя `entry_ids`
- **THEN** она не входит в суммы, `skipped_osv_entry_count` обеих направленных ячеек пары увеличивается на 2
#### Scenario: Эталон «Русский бисквит»
- **WHEN** матрица строится по ОСВ РБ и Пекарни Счастливочной из эталонных фикстур
- **THEN** суммы сравниваются с эталоном только для точных пар счетов и компаний; общая сумма элиминации эталона не приравнивается к минимуму по паре в API (решение владельца 08.10.2026)
### Requirement: XLSX шахматки
`GET …/intercompany-matrix/all/export` и `POST …/intercompany-matrix/manual/export` SHALL использовать
те же режимы, тело и права, что и соответствующие JSON-маршруты. Ответ — XLSX с именем
`intercompany-matrix_<ГК>_<год>-<квартал>_v<n>.xlsx`, шапкой (ГК, период, версия, единицы, режим,
выбранные счета; для ALL — «Все балансовые счета») и строками разницы, дебета, кредита и процента расхождения для
каждой пары. Пара SHALL заливаться цветом порога, если `exceeds_threshold` или `one_sided` — так же, как в API.
#### Scenario: Экспорт выбранных счетов
- **WHEN** пользователь выгружает MANUAL для ГК «Русский бисквит» за 2024 Q1, версия 1
- **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, шапка содержит выбранные счета, ячейки процента показывают два знака и символ `%` без повторного масштабирования
#### Scenario: Подсветка пары без встречной стороны
- **WHEN** выгружается ALL, у пары `amount_a = 100000000`, `amount_b = 0`
- **THEN** все строки пары залиты цветом порога, в шапке «Режим» и «Счета дебета/кредита» — «Все балансовые счета»

View File

@ -0,0 +1,8 @@
## 1. Реализация (PR RSHB/arm-core#165, AURORA-1651)
- [x] 1.1 Автор: `one_sided` по суммам, заливка xlsx по `exceeds_threshold or one_sided`, ALL без забалансовых счетов.
- [x] 1.2 Ревьюер: влив `main` (#141), конфликты; `one_sided` на отрицательных суммах; шапка ALL «Все балансовые счета»;
фильтр `/directories/accounts` убран; тесты (суммы, нули, отрицательные, xlsx, ALL/MANUAL с 001).
- [x] 1.3 Ревьюер: правила §6 и OQ в `../docs` (docs-PR вместе с этим change), гайд фронта §5.7.1.
- [x] 1.4 Ревьюер: подсветка `one_sided` на экране — PR #158, change `intercompany-matrix-ui`.
- [ ] 1.5 После слияния: архив change.