openspec: архив aurora-1883-closed-vsp и aurora-1896-cell-formulas

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-12 18:49:51 +04:00
parent fa3d7adad0
commit b1c61ae769
10 changed files with 172 additions and 0 deletions

View File

@ -0,0 +1,79 @@
# cell-formula Specification
## Purpose
Формулы ячеек листов бюджетных форм (FORM_1, FORM_2, FORM_4) и отчётов проектов (FORM_3): как формула сохраняется вместе со значением, кто и как её видит, когда она перестаёт показываться и когда удаляется.
Термины: **ячейка** — (владелец, лист, `line_id`, `column`), где владелец — бюджетная форма (`budget_form.id`) или отчёт проекта (`rf_project_report.id`); `line_id` и `column` — те же, что в `cell_updated` и `PATCH …/cell`. **Формула** — выражение без ведущего «=», например `100+50`.
## Requirements
### Requirement: Формула сохраняется вместе со значением ячейки
Запись ячейки с полем `formula` (WS `cell_updated` — `data.formula`; REST `PATCH /form/{id}/sheet/{sheet}/cell`, `…/cells`, `PATCH /projects/{id}/report/{year}/{type}/cell`, `…/cells` — поле `formula` элемента) SHALL сохранять формулу этой ячейки в той же транзакции, что и значение: выражение, значение (`value` записи), автора (текущий пользователь) и время записи. Предыдущая формула ячейки SHALL заменяться. Формула SHALL сохраняться только если запись значения прошла (те же проверки прав и принадлежности строки, что у значения); при отказе в записи значения формула SHALL NOT сохраняться.
#### Scenario: Сохранение формулы
- **WHEN** администратор пишет в ячейку FORM_2/AHR `plan.q1` значение 150 с формулой `100+50`
- **THEN** чтение формул листа возвращает для этой ячейки выражение `100+50`, значение 150, автора — этого администратора и время записи
#### Scenario: Отказ в праве на ячейку
- **WHEN** исполнитель без права на колонку (окно закрыто или чужая оргединица) пишет значение с формулой
- **THEN** запрос отклоняется так же, как без формулы (403)
- **AND** формула ячейки не появляется и прежняя формула не меняется
### Requirement: Ввод без формулы удаляет формулу ячейки
Запись значения ячейки без поля `formula` (или с `formula: null`) через любой из путей записи выше SHALL удалять формулу этой ячейки, если она была. Это касается и массовой записи `…/cells`: для каждого элемента без формулы формула его ячейки удаляется.
#### Scenario: Перезапись числом
- **WHEN** у ячейки есть формула `100+50`, и пользователь записывает в неё значение 200 без формулы
- **THEN** чтение формул листа не возвращает формулы этой ячейки
### Requirement: Проверка выражения
Сервер SHALL принимать выражение длиной от 1 до 500 символов, состоящее только из цифр, пробелов и символов `+ - * / % ( ) , .`; ведущий «=» и пробелы по краям SHALL отбрасываться при нормализации. Сервер SHALL NOT вычислять выражение. Формула с непустым выражением SHALL требовать числовое `value`. При нарушении сервер SHALL отвечать 422 с кодом ошибки по префиксу `invalid_formula:`, и значение ячейки в этом запросе SHALL NOT записываться.
#### Scenario: Недопустимое выражение
- **WHEN** пользователь пишет значение 150 с формулой `drop table`
- **THEN** ответ — 422 `invalid_formula`
- **AND** значение ячейки осталось прежним, формула не сохранилась
#### Scenario: Нормализация
- **WHEN** пользователь пишет формулу ` =100+50 `
- **THEN** сохраняется выражение `100+50`
### Requirement: Чтение формул листа
Сервер SHALL отдавать все формулы листа: `GET /api/v1/form/{form_id}/sheet/{sheet}/formulas` и `GET /api/v1/projects/{project_id}/report/{year}/{report_type}/formulas`. Каждая запись SHALL содержать `line_id`, `column`, `expression`, `value`, `updated_by` (`id`, `full_name`) и `updated_at`. Доступ SHALL проверяться так же, как у GET листа (форма) или отчёта (проект): нет доступа к форме или формы нет — 404 «Форма не найдена»; отчёт проекта не найден или нет доступа — та же ошибка, что у PATCH этого отчёта (400 «Отчёт не найден»).
#### Scenario: Формулы видны другому пользователю
- **WHEN** пользователь А сохранил формулу, а пользователь Б с доступом к форме запрашивает формулы листа
- **THEN** Б получает формулу А с её автором и временем
### Requirement: Рассылка формулы по WebSocket
Рассылка `cell_updated` всем подключённым к листу SHALL содержать в `data` поле `formula` — нормализованное сохранённое выражение или `null`, если формулы у ячейки после записи нет, — и при наличии формулы поле `formula_meta` с `updated_by` (`id`, `full_name`) и `updated_at`. Значение `data.formula` в рассылке SHALL задавать сервер, а не пересылать входящее значение клиента.
#### Scenario: Второй пользователь видит формулу без перезагрузки
- **WHEN** пользователь А вводит в ячейку `=100+50`
- **THEN** пользователь Б, открывший тот же лист, получает `cell_updated` с `data.formula = "100+50"` и автором А
- **AND** в ячейке у Б отображается 150 с маркером формулы
#### Scenario: Перезапись снимает маркер у всех
- **WHEN** пользователь Б записывает в ту же ячейку число 200
- **THEN** все подключённые клиенты получают `cell_updated` с `data.formula = null`, и маркер формулы пропадает
### Requirement: Отображение формулы
Фронт SHALL показывать у ячейки маркер формулы `fx` и подсказку с выражением, ФИО автора и датой-временем, только если для ячейки есть формула и её `value` совпадает с текущим значением ячейки с точностью 1e-9. Редактор ячейки SHALL открываться с текстом `=` + выражение при том же условии совпадения. Если значения не совпадают (значение изменилось не через ввод, например синхронизацией), маркер SHALL NOT показываться, и редактор открывается со значением.
#### Scenario: Формула устарела после пересчёта
- **WHEN** у ячейки есть формула со значением 150, а текущее значение ячейки стало 170
- **THEN** маркер формулы не показывается, редактор открывается со значением 170
### Requirement: Формулы не хранятся в браузере
Фронт SHALL NOT читать и писать формулы в localStorage. При первом запуске новой версии ключи `formula_*` SHALL удаляться из localStorage.
#### Scenario: Формула с другого компьютера
- **WHEN** автор сохранил формулу и открыл лист на другом компьютере
- **THEN** он видит маркер формулы и открывает редактор с `=выражением`
### Requirement: Формулы удаляются вместе с владельцем и строкой
Формулы SHALL удаляться при удалении строки, к которой относится ячейка (строка бюджета, запись сателлита аренды/коммунальных/охраны, запись отчёта ОС OTCH9F, строка отчёта проекта), при окончательном удалении бюджетной формы и при удалении отчёта проекта — независимо от того, какая функция удаляет строку.
#### Scenario: Удаление строки
- **WHEN** у строки листа есть формулы, и строку удаляют
- **THEN** чтение формул листа не возвращает формул этой строки

View File

@ -0,0 +1,93 @@
# vsp-directory Specification
## Purpose
Справочник объектов ВСП (`/api/v1/dict/info`, страница «Справочник ВСП») и подпись ВСП в листах форм: какие ВСП попадают в список, выгрузку и выпадающие списки, как их фильтровать по статусу и периоду закрытия, как закрытый ВСП выглядит в справочнике и в ячейке формы.
Термины: **действующий ВСП** — запись `v3.vsp` с `is_deleted = false` и `is_active = true`; **закрытый ВСП** — `is_deleted = false` и `is_active = false` (дата закрытия наступила); **удалённый ВСП** — `is_deleted = true`.
## Requirements
### Requirement: Фильтр справочника по статусу
`GET /api/v1/dict/info` SHALL принимать query-параметр `status` со значениями `active`, `closed`, `all`; по умолчанию — `active`. `active` SHALL возвращать только действующие ВСП, `closed` — только закрытые, `all` — действующие и закрытые. Иное значение SHALL давать 422. Удалённые ВСП SHALL NOT возвращаться ни при каком значении `status`. Остальные фильтры (рег. номер, адрес, ССП, дата открытия, тип размещения, численность) и ограничение исполнителя своими ССП SHALL применяться как прежде, вместе со `status`.
#### Scenario: По умолчанию — только действующие
- **WHEN** администратор запрашивает `GET /dict/info` без `status`, а в справочнике есть действующий ВСП A и закрытый ВСП B
- **THEN** ответ содержит A и не содержит B
#### Scenario: Закрытые
- **WHEN** администратор запрашивает `GET /dict/info?status=closed`
- **THEN** ответ содержит B с `is_active = false` и не содержит A
#### Scenario: Все
- **WHEN** администратор запрашивает `GET /dict/info?status=all`
- **THEN** ответ содержит и A, и B
#### Scenario: Недопустимый статус
- **WHEN** запрос `GET /dict/info?status=bogus`
- **THEN** ответ — 422
#### Scenario: Удалённый не возвращается
- **WHEN** администратор удалил ВСП C (`DELETE /dict/info/{id}`) и запрашивает `status=active`, `closed` и `all`
- **THEN** ни один ответ не содержит C
#### Scenario: Исполнитель видит закрытые только своих ССП
- **WHEN** исполнитель, привязанный к ССП S1, запрашивает `GET /dict/info?status=closed`, а закрытые ВСП есть в S1 и в чужом S2
- **THEN** ответ содержит закрытые ВСП S1 и не содержит закрытые ВСП S2
### Requirement: Фильтр по периоду закрытия
`GET /api/v1/dict/info` SHALL принимать необязательные `close_date_start` и `close_date_end` (даты `YYYY-MM-DD`). При заданном `close_date_start` SHALL возвращаться только ВСП с датой закрытия не раньше неё, при заданном `close_date_end` — не позже неё (границы включительно). ВСП без даты закрытия SHALL NOT попадать в ответ, если задана хотя бы одна граница.
#### Scenario: ВСП закрыт в периоде
- **WHEN** ВСП B закрыт 2021-03-15 и запрос `GET /dict/info?status=closed&close_date_start=2021-01-01&close_date_end=2021-12-31`
- **THEN** ответ содержит B
#### Scenario: ВСП закрыт вне периода
- **WHEN** тот же ВСП B и запрос `?status=closed&close_date_start=2022-01-01`
- **THEN** ответ не содержит B
### Requirement: Порядок закрытых ВСП
При `status=closed` список SHALL быть отсортирован по дате закрытия по убыванию, при равной дате — по `id` по возрастанию. При `status=active` и `status=all` порядок SHALL оставаться прежним — по `id`.
#### Scenario: Свежезакрытые сверху
- **WHEN** ВСП B закрыт 2021-03-15, ВСП D закрыт 2024-06-01 и запрос `?status=closed`
- **THEN** D идёт в ответе раньше B
### Requirement: Выгрузка учитывает статус и период
`POST /api/v1/dict/info/export` SHALL принимать в теле `status` (`active` | `closed` | `all`, по умолчанию `active`), `close_date_start`, `close_date_end` и применять их так же, как `GET /dict/info`. Без этих полей выгрузка SHALL содержать только действующие ВСП, как раньше.
#### Scenario: Выгрузка закрытых
- **WHEN** администратор выгружает справочник с `status = closed`
- **THEN** файл содержит закрытый ВСП B и не содержит действующий A
### Requirement: Выпадающий список ВСП с закрытыми для подписей
`GET /api/v1/dict/info/dropdown` SHALL принимать параметр `include_inactive` (по умолчанию `false`). При `false` SHALL возвращаться только действующие ВСП, как раньше; при `true` — действующие и закрытые в тех же границах (ССП формы `form_id` или `ssp_id`, ССП исполнителя). Удалённые ВСП SHALL NOT возвращаться.
#### Scenario: Закрытый ВСП ССП формы
- **WHEN** у ССП формы F есть закрытый ВСП B и запрос `GET /dict/info/dropdown?form_id=F&include_inactive=true`
- **THEN** ответ содержит B с `is_active = false`
#### Scenario: Без параметра
- **WHEN** запрос `GET /dict/info/dropdown?form_id=F`
- **THEN** ответ не содержит B
### Requirement: Справочник ВСП показывает статус и позволяет найти закрытые
Страница «Справочник ВСП» SHALL иметь переключатель статуса «Действующие» (по умолчанию) / «Закрытые» / «Все» и, при «Закрытые» и «Все», поля периода закрытия «Закрыт с» / «по». Смена статуса или периода SHALL перезагружать список с сервера с этими параметрами. Таблица SHALL иметь колонку «Статус» с бейджем «Действует» или «Закрыт»; текст строк закрытых ВСП SHALL быть приглушённым. Выгрузка со страницы SHALL передавать текущие статус и период. Если после правки ВСП перестал подходить под выбранный статус (например, его закрыли в режиме «Действующие»), строка SHALL исчезать из списка.
#### Scenario: Найти давно закрытый ВСП
- **WHEN** администратор выбирает «Закрытые» и период «Закрыт с 01.01.2021 по 31.12.2021»
- **THEN** таблица показывает ВСП, закрытые в 2021 году, с бейджем «Закрыт»
#### Scenario: Закрыли в режиме «Действующие»
- **WHEN** в режиме «Действующие» администратор ставит ВСП дату закрытия «вчера» и сохраняет
- **THEN** строка этого ВСП пропадает из таблицы
### Requirement: Подпись ВСП в листах форм
Ячейка колонки с `editType = 'vsp_dropdown'` в листах форм SHALL показывать рег. номер ВСП, а не число: для действующего ВСП — рег. номер; для закрытого — «<рег. номер> (закрыт)»; если ВСП с таким id нет среди загруженных для формы (например, удалён) — «ВСП #<id>». Значение такой ячейки SHALL NOT форматироваться как сумма и SHALL NOT масштабироваться селектором разрядности. Для выбора ВСП в ячейке и при добавлении строки SHALL предлагаться только действующие ВСП. Поиск и фильтр по колонке ВСП SHALL находить и закрытые ВСП по рег. номеру.
#### Scenario: Строка с закрытым ВСП
- **WHEN** в листе FORM_2/AHR строка ссылается на закрытый ВСП с рег. номером `1234/567`, разрядность «тыс. руб.»
- **THEN** ячейка «ВСП РФ» показывает `1234/567 (закрыт)`
#### Scenario: Выбор ВСП
- **WHEN** пользователь открывает выбор ВСП в ячейке этой строки
- **THEN** в списке только действующие ВСП, закрытого `1234/567` в нём нет