diff --git a/openspec/changes/aurora-1883-closed-vsp/.openspec.yaml b/openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/.openspec.yaml similarity index 100% rename from openspec/changes/aurora-1883-closed-vsp/.openspec.yaml rename to openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/.openspec.yaml diff --git a/openspec/changes/aurora-1883-closed-vsp/design.md b/openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/design.md similarity index 100% rename from openspec/changes/aurora-1883-closed-vsp/design.md rename to openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/design.md diff --git a/openspec/changes/aurora-1883-closed-vsp/proposal.md b/openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/proposal.md similarity index 100% rename from openspec/changes/aurora-1883-closed-vsp/proposal.md rename to openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/proposal.md diff --git a/openspec/changes/aurora-1883-closed-vsp/specs/vsp-directory/spec.md b/openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/specs/vsp-directory/spec.md similarity index 100% rename from openspec/changes/aurora-1883-closed-vsp/specs/vsp-directory/spec.md rename to openspec/changes/archive/2026-11-12-aurora-1883-closed-vsp/specs/vsp-directory/spec.md diff --git a/openspec/changes/aurora-1896-cell-formulas/.openspec.yaml b/openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/.openspec.yaml similarity index 100% rename from openspec/changes/aurora-1896-cell-formulas/.openspec.yaml rename to openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/.openspec.yaml diff --git a/openspec/changes/aurora-1896-cell-formulas/design.md b/openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/design.md similarity index 100% rename from openspec/changes/aurora-1896-cell-formulas/design.md rename to openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/design.md diff --git a/openspec/changes/aurora-1896-cell-formulas/proposal.md b/openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/proposal.md similarity index 100% rename from openspec/changes/aurora-1896-cell-formulas/proposal.md rename to openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/proposal.md diff --git a/openspec/changes/aurora-1896-cell-formulas/specs/cell-formula/spec.md b/openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/specs/cell-formula/spec.md similarity index 100% rename from openspec/changes/aurora-1896-cell-formulas/specs/cell-formula/spec.md rename to openspec/changes/archive/2026-11-12-aurora-1896-cell-formulas/specs/cell-formula/spec.md diff --git a/openspec/specs/cell-formula/spec.md b/openspec/specs/cell-formula/spec.md new file mode 100644 index 0000000..2fb1d62 --- /dev/null +++ b/openspec/specs/cell-formula/spec.md @@ -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** чтение формул листа не возвращает формул этой строки diff --git a/openspec/specs/vsp-directory/spec.md b/openspec/specs/vsp-directory/spec.md new file mode 100644 index 0000000..7eca761 --- /dev/null +++ b/openspec/specs/vsp-directory/spec.md @@ -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 нет среди загруженных для формы (например, удалён) — «ВСП #». Значение такой ячейки SHALL NOT форматироваться как сумма и SHALL NOT масштабироваться селектором разрядности. Для выбора ВСП в ячейке и при добавлении строки SHALL предлагаться только действующие ВСП. Поиск и фильтр по колонке ВСП SHALL находить и закрытые ВСП по рег. номеру. + +#### Scenario: Строка с закрытым ВСП +- **WHEN** в листе FORM_2/AHR строка ссылается на закрытый ВСП с рег. номером `1234/567`, разрядность «тыс. руб.» +- **THEN** ячейка «ВСП РФ» показывает `1234/567 (закрыт)` + +#### Scenario: Выбор ВСП +- **WHEN** пользователь открывает выбор ВСП в ячейке этой строки +- **THEN** в списке только действующие ВСП, закрытого `1234/567` в нём нет