diff --git a/openspec/changes/intercompany-matrix/.openspec.yaml b/openspec/changes/archive/2026-10-16-intercompany-matrix/.openspec.yaml similarity index 100% rename from openspec/changes/intercompany-matrix/.openspec.yaml rename to openspec/changes/archive/2026-10-16-intercompany-matrix/.openspec.yaml diff --git a/openspec/changes/intercompany-matrix/design.md b/openspec/changes/archive/2026-10-16-intercompany-matrix/design.md similarity index 100% rename from openspec/changes/intercompany-matrix/design.md rename to openspec/changes/archive/2026-10-16-intercompany-matrix/design.md diff --git a/openspec/changes/intercompany-matrix/proposal.md b/openspec/changes/archive/2026-10-16-intercompany-matrix/proposal.md similarity index 100% rename from openspec/changes/intercompany-matrix/proposal.md rename to openspec/changes/archive/2026-10-16-intercompany-matrix/proposal.md diff --git a/openspec/changes/intercompany-matrix/specs/audit-trail/spec.md b/openspec/changes/archive/2026-10-16-intercompany-matrix/specs/audit-trail/spec.md similarity index 100% rename from openspec/changes/intercompany-matrix/specs/audit-trail/spec.md rename to openspec/changes/archive/2026-10-16-intercompany-matrix/specs/audit-trail/spec.md diff --git a/openspec/changes/intercompany-matrix/specs/consolidated-osv/spec.md b/openspec/changes/archive/2026-10-16-intercompany-matrix/specs/consolidated-osv/spec.md similarity index 100% rename from openspec/changes/intercompany-matrix/specs/consolidated-osv/spec.md rename to openspec/changes/archive/2026-10-16-intercompany-matrix/specs/consolidated-osv/spec.md diff --git a/openspec/changes/intercompany-matrix/specs/consolidations-api/spec.md b/openspec/changes/archive/2026-10-16-intercompany-matrix/specs/consolidations-api/spec.md similarity index 100% rename from openspec/changes/intercompany-matrix/specs/consolidations-api/spec.md rename to openspec/changes/archive/2026-10-16-intercompany-matrix/specs/consolidations-api/spec.md diff --git a/openspec/changes/intercompany-matrix/specs/directories-api/spec.md b/openspec/changes/archive/2026-10-16-intercompany-matrix/specs/directories-api/spec.md similarity index 100% rename from openspec/changes/intercompany-matrix/specs/directories-api/spec.md rename to openspec/changes/archive/2026-10-16-intercompany-matrix/specs/directories-api/spec.md diff --git a/openspec/changes/intercompany-matrix/tasks.md b/openspec/changes/archive/2026-10-16-intercompany-matrix/tasks.md similarity index 100% rename from openspec/changes/intercompany-matrix/tasks.md rename to openspec/changes/archive/2026-10-16-intercompany-matrix/tasks.md diff --git a/openspec/specs/audit-trail/spec.md b/openspec/specs/audit-trail/spec.md index 05b12ba..085fe86 100644 --- a/openspec/specs/audit-trail/spec.md +++ b/openspec/specs/audit-trail/spec.md @@ -145,3 +145,15 @@ ORM-модель `audit_event` MUST NOT использоваться для `UPD #### Scenario: Автоматический переход - **WHEN** подтверждение документа перевело версию в `DATA_CORRECTION` - **THEN** событие `CONSOLIDATION_STATUS_CHANGE` с `user_id` подтвердившего, `before_value.status = INITIATED` + +### Requirement: Аудит экспорта шахматки +Каждый успешный `GET …/intercompany-matrix/all/export` и `POST …/intercompany-matrix/manual/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}`. `mode` — `ALL` или `MANUAL`, массивы счетов — ID из запроса (в ALL пустые), +`cells` — число ячеек. Выгрузка сводной ОСВ продолжает писать собственное событие `EXPORT`. + +#### Scenario: Две выгрузки +- **WHEN** пользователь дважды выгружает ту же шахматку +- **THEN** в `audit_event` две разные записи `EXPORT`, каждая со своим payload и числом ячеек diff --git a/openspec/specs/consolidated-osv/spec.md b/openspec/specs/consolidated-osv/spec.md index 2dbd2bf..c6e648f 100644 --- a/openspec/specs/consolidated-osv/spec.md +++ b/openspec/specs/consolidated-osv/spec.md @@ -162,3 +162,51 @@ filename="svod-osv.xlsx"; filename*=UTF-8''svod-osv_<ГК>_<год>-<кварт - **WHEN** экспорт версии с 3 строками и 1 пробелом открыт `openpyxl` - **THEN** лист «Сводная ОСВ» содержит заголовок и 3 строки, суммы — числа с двумя знаками, равные API; лист «Пробелы» — одну строку с `Q2`; `audit_event(EXPORT)` с `after_value.rows = 3` + +### 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 включает все счета вида `A`/`AP` для дебета и `P`/`AP` для кредита. MANUAL принимает +обязательные непустые `debit_account_ids: UUID[]` и `credit_account_ids: UUID[]`, раскрывает выбранные счета +по иерархии и оставляет подходящий вид. Неизвестный счёт 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 при наличии исходных записей ОСВ только с одной стороны, +в том числе при нулевой сумме. `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** у пары есть строка свода с `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.xlsx`, шапкой (ГК, период, версия, единицы, режим, +выбранные счета) и строками разницы, дебета, кредита и процента расхождения для каждой пары. + +#### Scenario: Экспорт выбранных счетов +- **WHEN** пользователь выгружает MANUAL для ГК «Русский бисквит» за 2024 Q1, версия 1 +- **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, шапка содержит выбранные счета, ячейки процента показывают два знака и символ `%` без повторного масштабирования diff --git a/openspec/specs/consolidations-api/spec.md b/openspec/specs/consolidations-api/spec.md index 19331bd..5b64bd6 100644 --- a/openspec/specs/consolidations-api/spec.md +++ b/openspec/specs/consolidations-api/spec.md @@ -223,3 +223,31 @@ account, status, missing_quarters, document_ids}`: `document_type` и `account` #### Scenario: Невидимая версия при занятой строке - **WHEN** версию чужого филиала держит `FOR UPDATE` другая транзакция, пользователь без видимости вызывает `POST …/lock` - **THEN** 404 `VERSION_NOT_FOUND` без ожидания + +### Requirement: Правила элиминации версии +`GET /api/v1/consolidations/versions/{version_id}/elimination-rules` (`CONSOLIDATION_VIEW`, policy по ГК) +SHALL возвращать все правила версии массивом. Каждое правило SHALL содержать `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` + +### Requirement: Результирующая таблица ВГО +`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table` +(`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{companies[], articles[]}`. Компании — все члены периметра +по имени; статья содержит `code`, `name`, `amounts[] {company_id, amount}` и `group_total`. +Пять фиксированных статей §6: дебиторская и кредиторская задолженность, финансовые вложения, +долгосрочные и краткосрочные займы. Сумма компании SHALL считаться по всем остальным компаниям периметра +из конечного сальдо нужной стороны и выбранных счетов статьи; `group_total` — сумма по компаниям. +Отсутствие любого необходимого кода счёта SHALL давать 422 `RESULT_ACCOUNTS_MISSING` с перечнем кодов. + +#### Scenario: Компания без ОСВ +- **WHEN** в периметре есть компания без подходящих строк ОСВ +- **THEN** она есть в `companies`, её сумма по каждой статье — строка `"0"` + +#### Scenario: Нет обязательного счёта +- **WHEN** в плане счетов отсутствует счёт 58.03 +- **THEN** ответ 422 `RESULT_ACCOUNTS_MISSING`, сообщение содержит `58.03` diff --git a/openspec/specs/directories-api/spec.md b/openspec/specs/directories-api/spec.md index f9491f3..245fa18 100644 --- a/openspec/specs/directories-api/spec.md +++ b/openspec/specs/directories-api/spec.md @@ -189,3 +189,18 @@ SHALL выполняться в одной транзакции с запись #### Scenario: Ошибка внутри мутации - **WHEN** при переводе ЮЛ закрытие членства завершается ошибкой БД - **THEN** ЮЛ остаётся в прежней ГК, строк истории и аудита нет + +### Requirement: Плоский список счетов для фильтров шахматки +`GET /api/v1/directories/accounts` SHALL возвращать пользователю с `DIRECTORY_VIEW` все строки +`chart_of_account` одним массивом в естественном порядке кодов. Каждая строка SHALL содержать `id`, `code`, +`parent_id`, `name`, `kind` (`A`/`P`/`AP`/null), `row_type`, `is_off_balance`, `is_debit`, `is_credit`; +корневые счета имеют `parent_id = null`. `is_debit = true` для `A` и `AP`, `is_credit = true` для `P` и `AP`; +при `kind = null` оба признака ложны. + +#### Scenario: Счета разных уровней +- **WHEN** в плане счетов есть счёт, субсчёт и аналитика +- **THEN** ответ содержит все три строки без вложенности, а `parent_id` указывает на родительскую строку + +#### Scenario: Нет права на справочник +- **WHEN** пользователь без `DIRECTORY_VIEW` запрашивает список счетов +- **THEN** ответ 403