From e500c85e81176ff5dec0a5f4277c7340e07424e8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 8 Nov 2026 21:28:34 +0400 Subject: [PATCH] =?UTF-8?q?AURORA-1984:=20OpenSpec=20change=20=E2=80=94=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D1=8B=20=D0=9F=D0=94=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=BC=D1=8B=20(+1979,=201980,=201989)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #305 Co-Authored-By: Claude Opus 5.5 --- .../.openspec.yaml | 2 + .../design.md | 127 ++++++++++++++++++ .../proposal.md | 42 ++++++ .../specs/form4-programs-projects/spec.md | 92 +++++++++++++ 4 files changed, 263 insertions(+) create mode 100644 openspec/changes/aurora-1984-pd-standalone-projects/.openspec.yaml create mode 100644 openspec/changes/aurora-1984-pd-standalone-projects/design.md create mode 100644 openspec/changes/aurora-1984-pd-standalone-projects/proposal.md create mode 100644 openspec/changes/aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/.openspec.yaml b/openspec/changes/aurora-1984-pd-standalone-projects/.openspec.yaml new file mode 100644 index 0000000..c0ad5ed --- /dev/null +++ b/openspec/changes/aurora-1984-pd-standalone-projects/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-08 diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/design.md b/openspec/changes/aurora-1984-pd-standalone-projects/design.md new file mode 100644 index 0000000..25e1128 --- /dev/null +++ b/openspec/changes/aurora-1984-pd-standalone-projects/design.md @@ -0,0 +1,127 @@ +# Design + +## Context + +Форма 4 (FORM_4) в БД — «Смета РФ поддержка», на фронте — «Проектная деятельность». Листы AHR/CAP/OPER строятся `v3.v_form4_sheet_jsonb` (последняя версия — `sql/0053_form2_ahr_source_sheet.sql:840`). Иерархия листа: ROOT (раздел, depth 0) → GROUP (программа, depth 1) → ITEM (проект, depth 2) → INPUT (расход, `budget_line`, depth 3). `sort_order` — `ROW_NUMBER() OVER (ORDER BY sort_path)`, где `sort_path` у INPUT — `ARRAY[sec_id, COALESCE(prog_id, 999999), …, line_id]`. ROOT выводится только если в разделе есть хотя бы один расход (`root_names` из `inp`); пустые программы и проекты выводятся из `all_programs` / `all_projects`. + +Таблица `v3.form4_project` (`sql/0010_form4.sql:25`): `level IN ('program','project')`, `parent_id` (self-FK), `section_code`, `form_id`. Ограничения: `chk_form4_project_parent` — у программы `parent_id IS NULL`; `chk_form4_project_section_code` — `(level = 'program') = (section_code IS NOT NULL)`. `budget_line.project_id` → `form4_project.id` (FK); расход может ссылаться и на программу напрямую (ветка «B» в `inp`, исторические данные). + +Операции (`sql/0024_upd_form4_projects.sql`, `sql/0026_budget_line_stage_access.sql:486`): `add_form4_program(name, form_id, section)`, `add_form4_project(name, program_id)`, `upd_/del_form4_program`, `upd_/del_form4_project`. Все возвращают `(project_id, row_type, depth, sort_order, data)` — затронутые строки листа. WebSocket (`api/src/api/v1/websocket.py:470-505`): `program_added`, `project_added` (`{name, program_id}`), `program_updated`, `program_deleted`, `project_updated`, `project_deleted`; рассылка всем подключённым к листу. + +Ответ на правку ячейки для FORM_4 — `v3._form_affected_rows` (`sql/0052_…:3293`): полный лист, отбор INPUT + ROOT + GROUP по `program_id` + ITEM по `project_id` изменённых строк. + +Фронт: `web/src/components/RealtimeTable/` — `RealtimeTable.jsx` (кнопки «Добавить программу/проект», `getDeleteTarget` из AURORA-1978), `hooks/useRealtimeData.js` (обработчики событий, `insertProjectRow` по `program_id`), `utils/cellUtils.js` (`alignNodeRows`, `deleteProgramRow`/`deleteProjectRow` из AURORA-1978), `utils/rowUtils.js` (`buildRowHierarchy` по `row_type`, переживает пропуск уровня). Выгрузка Excel (`api/src/services/export_service/`) берёт те же строки листа, иерархию строит по `row_type` (`export_writers._hierarchical_rows`). + +Кто ещё читает программы/проекты — проверено 08.11: своды (`v_summary_view`), SMETA (`v_form4_smeta`) и STRUCTURE (`v_form4_view`, ветка STRUCTURE) — нет; `hard_delete_budget_form` удаляет по `form_id`; `add_budget_line` проверяет только существование `project_id`. + +Ветка `feat/305-aurora-1984-pd-standalone-projects` начата от `feat/303-aurora-1978-pd-delete-program-project` (PR #304, не влит на 08.11). + +## Goals / Non-Goals + +**Goals** +- Самостоятельный проект: создание, расходы, переименование, удаление пустого, место в листе в порядке создания, суммы в ROOT ровно один раз, выгрузка. +- AURORA-1979, 1980, 1989 (см. proposal). + +**Non-Goals** +- Перенос существующего проекта в программу и из неё. +- Миграция существующих данных: проекты в программах не трогаются. +- Расходы, привязанные прямо к программе (ветка «B»), — поведение не меняется. +- Изменение сводов, листов SMETA/STRUCTURE. + +**Anti-goals** (сломать — хуже, чем не сделать) +- Фиктивная GROUP с пустым `program_id` в листе или выгрузке; двойной учёт сумм самостоятельного проекта в ROOT. +- Любое изменение вида листа и сумм для форм без самостоятельных проектов. + +## Decisions + +### D1. Модель: проект без родителя хранит раздел сам +`form4_project(level='project', parent_id NULL, section_code NOT NULL, form_id NOT NULL)`. Ограничение `chk_form4_project_section_code` пересоздаётся как `((level = 'program') OR (parent_id IS NULL)) = (section_code IS NOT NULL)`. Новое ограничение `chk_form4_project_standalone_form`: `parent_id IS NOT NULL OR form_id IS NOT NULL` (у программы и самостоятельного проекта форма обязательна; существующие программы её имеют — проверить запросом в миграции перед `ADD CONSTRAINT`). Существующие строки обоим ограничениям удовлетворяют (у всех проектов есть родитель и нет раздела). Отдельный тип/флаг не вводится: «самостоятельный» = `level='project' AND parent_id IS NULL`. + +### D2. `add_form4_project` — одна функция, программа или форма+раздел +Новая сигнатура: `v3.add_form4_project(p_name varchar, p_program_id int DEFAULT NULL, p_form_id int DEFAULT NULL, p_section_code varchar DEFAULT NULL)`; старая `(varchar, int)` удаляется (`DROP FUNCTION`), вызовы с двумя аргументами продолжают работать за счёт DEFAULT. Ровно один из вариантов: `p_program_id` или пара `p_form_id` + `p_section_code`; иначе `RAISE`. Для самостоятельного: проверка, что форма — FORM_4 и раздел — один из AHR/CAP/OPER; права — `v3.can_edit(p_form_id, p_section_code, NULL, NULL, user)` с теми же кодами ошибок, что сейчас; аудит `PROJECT_CREATE` с `parent_id: null`, `section_code`. Возвращает строки так же, как вариант с программой: новый ITEM + ROOT раздела (если он есть в листе). + +### D3. Лист: ITEM под ROOT, порядок по id верхнего узла +В `v3.v_form4_sheet_jsonb`: +- `inp`: для расхода самостоятельного проекта `prj_id` = проект (соединение `prj` по `level='project'` его находит), `prog_via_prj` и `prog_direct` пусты → `prog_id` = NULL. +- `all_projects`: добавить самостоятельные проекты листа (`parent_id IS NULL AND section_code = p_sheet AND form_id = p_form_id`, с `p_sections`); сейчас там `JOIN all_programs`, самостоятельные туда не попадают. +- Агрегаты (блок «4», GROUPING SETS по раздел / программа / проект). Уровень программы (`lvl = 1`) уже отсекает `prog_id IS NULL` (`0053:1248`) — фиктивной GROUP не будет. **Уровень проекта соединяется по `d.prog_id = n.prog_id AND d.prj_id = n.prj_id` (`0053:1289`) — при `prog_id IS NULL` равенство ложно, суммы ITEM самостоятельного проекта потеряются.** Заменить сравнение `prog_id` на `IS NOT DISTINCT FROM` во всех соединениях уровня проекта (`prj_names` ↔ агрегаты, пустые проекты из `all_projects`); для строк с непустой программой результат идентичен. ROOT суммирует все расходы раздела один раз (как сейчас). +- Исторические расходы FORM_4 без проекта и программы (`project_id IS NULL`) остаются как есть: `sort_path` с `999999`, только в ROOT. Самостоятельные проекты используют реальные id (< 999999), пересечения нет. Тест: лист с таким расходом не меняется. +- `sort_path` на уровне под ROOT — id верхнего узла: для программы `prog_id`, для самостоятельного проекта `prj_id`. Id из одной последовательности → порядок создания. Пример: GROUP `[sec, prog_id]`, ITEM в программе `[sec, prog_id, prj_id]`, самостоятельный ITEM `[sec, prj_id, 0]`, его INPUT `[sec, prj_id, 0, line_id]` — точную форму массивов подбирает исполнитель так, чтобы проект шёл сразу за своими расходами и ничего не пересекалось с программами. Сортировка программ и проектов в программах не меняется. +- ITEM самостоятельного проекта: `depth = 2` (как у проекта в программе), `header`: `section_code`, `name`, `program_id: null`, `project_id`, `expense_item_id` раздела. +Тело функции — от версии 0053 (последней), меняются только перечисленные блоки. + +### D4. Правка и удаление без родителя +`upd_form4_project` / `del_form4_project`: раздел — `COALESCE(программа.section_code, проект.section_code)`; в возвращаемых строках GROUP только при наличии родителя. `del_form4_project` — прежний запрет при расходах. `upd/del_form4_program` не меняются. + +### D4a. Контракт ответа, когда ROOT раздела нет +ROOT выводится только при наличии расходов в разделе (как сейчас, не меняется). Ответ `add_/upd_/del_form4_project` для самостоятельного проекта: затронутый ITEM (кроме удаления) и ROOT раздела, **если ROOT есть в листе после операции**; `sort_order` — глобальный, из полной сборки листа (как у всех функций, строящих ответ из `v_form4_sheet_jsonb`). В пустом разделе ответ на создание — один ITEM. Первый расход в разделе возвращает ROOT существующим путём `add_budget_line` → фронт добавляет ROOT (`addRootRow` в `useRealtimeData`), это поведение уже есть. Фронт вставляет ITEM под ROOT, если ROOT есть локально, иначе на верхний уровень. + +### D5. `_form_affected_rows` не меняется +Для расхода самостоятельного проекта `program_id` в header INPUT пустой → GROUP не отбирается, ITEM — по `project_id`, ROOT — по пути статей. Покрывается интеграционным тестом (спека: «Правка расхода»). Если тест покажет иначе — правка в той же миграции. + +### D6. Сервер (Python) +- `websocket.py` `__add_project`: `program_id` необязателен; без него — `form_id` и `sheet` соединения передаются в сервис. +- `SheetService.add_project`: без программы — проверка доступа по форме, как в `add_program` (не админ → форма среди оргединиц пользователя), затем `sheet_repo.add_project(name, program_id=None, form_id, section_code=sheet)`. +- `SheetRepository.add_project` — вызов новой сигнатуры с именованными параметрами. +- Ответ `project_added`: `[project_id, rows]` как сейчас. + +### D7. Фронт: создание и вставка +- «Добавить проект» доступна без выделения. Цель по выделенной строке (функция рядом с `getDeleteTarget` в `rowUtils.js`, с unit-тестом): GROUP → `program_id` этой программы; ITEM/INPUT → `program_id` из header (может быть null → в разделе); ROOT или ничего → в разделе. Отправка `project_added {name}` или `{name, program_id}`. +- Обработка ответа: новый ITEM — в `subRows` программы (как сейчас) или под ROOT раздела, либо на верхний уровень, если ROOT в локальном дереве нет. Позиция — по `sort_order` среди соседей; агрегаты из ответа вливаются через `alignNodeRows` (сопоставление по id, а не по `sort_order`, см. AURORA-1978). Если в ответе пришёл ROOT, которого локально нет, — добавить его (аналог `addRootRow`). +- Удаление — без изменений (AURORA-1978 работает по `project_id`). +- Добавление расхода к самостоятельному проекту — существующий путь без правок: `SelectExpenseItemModal` берёт статьи по листу (`ExpenseItemApi.getAll({ r_start, sheet })`), раздел самостоятельного проекта = лист; `add_budget_line` раздел по проекту не проверяет, только существование `project_id`; `handleAddExpenseItemRow` берёт `project_id` из header ITEM. +- Одновременное создание проектов двумя пользователями: сервер упорядочивает по id; клиент вставляет по `sort_order` из ответа; возможное расхождение позиции до перезагрузки допустимо, отдельных тестов гонки не делаем. + +### D7a. Вид в выгрузке и на экране +Самостоятельный ITEM: отступ названия и раскраска — как у проекта (`row_type` ITEM, `depth` 2); уровень группировки (outline) в Excel — как у программы (1), это получается само из `export_writers._hierarchical_rows` (стек по `row_type`), правка писателя не нужна. На экране свёртка — по `row_type` (`buildRowHierarchy`), так же. Эталон теста выгрузки: строки самостоятельного проекта — ITEM под ROOT, без GROUP, outline 1, расходы под ним outline 2. + +### D8. AURORA-1980: переименование +- Кнопка «Переименовать» в группе «Строки» панели (только FORM_4), активна при выделенной программе или проекте. Окно — `CreateProgramModal` с начальным значением и заголовком «Переименование программы/проекта» (или отдельный компонент — на усмотрение исполнителя). +- Отправка `program_updated {program_id, name}` / `project_updated {project_id, name}` с обрезанным названием; пустое после обрезки — кнопка сохранения неактивна. +- Окно закрывается по успешному ответу на своё событие; при `error` в ответе — остаётся открытым с введённым текстом, тост с причиной (`getWsErrorMessage`). Сопоставление ответа с запросом — по событию и id узла (WS без идентификатора запроса). +- Обработчики ответов `program_updated` / `project_updated` (их сейчас нет на фронте): имя в строке и агрегаты через `alignNodeRows` + `updateCells`; рассылка приходит всем подключённым. + +### D9. AURORA-1979: подпись в третьем столбце +Колонка «Код статьи / подстатьи / позиции» листов AHR/CAP/OPER формы 4: для GROUP с `program_id` — «программа», для ITEM с `project_id` — «проект»; иначе — как сейчас. Чистая функция подписи (unit-тест) в конфиге колонок FORM_4. В выгрузке Excel — та же подпись в той же колонке (нормализатор строк FORM_4 в export_service). `header.section_code` в данных не меняется. + +### D10. AURORA-1989: окно выбора расхода +`SelectExpenseItemModal`: подпись варианта `«${item_id} — ${name}»` (если `item_id` пуст — только название); сортировка на клиенте естественная по `item_id` (числовые части сравниваются как числа: «3.2.» < «3.10.»), без кода — в конце; поиск MUI Autocomplete по подписи (номер и название). Чистые функции подписи/сравнения — в utils с unit-тестами. Серверный запрос не меняется. + +### D11. Миграция +`api/alembic/versions/0059_form4_standalone_project.py` + `sql/0059_form4_standalone_project.sql`. Порядок: ограничение → `DROP FUNCTION add_form4_project(varchar, int4)` → новая `add_form4_project` → `upd_/del_form4_project` → `v_form4_sheet_jsonb`. Downgrade — возврат версий из 0024/0026/0053 и прежнего ограничения (падает, если есть самостоятельные проекты — это ожидаемо, текст ошибки понятный). Номер 0059 может сдвинуться при rebase на свежий `test`. + +## Тестирование + +- Интеграционные (`api/tests/integration/test_form4_standalone_project.py`, по образцу `test_form4_sheet_sums.py`): создание самостоятельного проекта (WS или SQL + GET листа) — ITEM под ROOT, нет GROUP с пустым `program_id`; порядок «программа, проект, программа» по созданию; суммы ROOT = программа + самостоятельный проект; правка ячейки расхода — состав ответа (INPUT, ITEM, ROOT, без GROUP) и равенство `data` с GET; переименование и удаление (пустой — ок, с расходом — отказ); права — исполнитель чужой формы получает отказ на создание, переименование и удаление самостоятельного проекта; создание с пустым названием — отказ; вызов с двумя аргументами (программа) работает как раньше; лист с историческим расходом без проекта и форма без самостоятельных проектов — GET побайтно равен результату до миграции (эталон снимается до миграции в том же тесте или фикстурой). +- Существующие тесты FORM_4 (`test_form4_sheet_sums.py`, `test_form4_transfer_econ.py`) — зелёные без правок. +- Фронт (`node --test`): цель «Добавить проект» по выделению; вставка ITEM под ROOT / на верхний уровень; подпись третьего столбца; подпись/сортировка/фильтр статей расходов. +- Выгрузка: unit или интеграционный тест — самостоятельный проект в Excel без лишней строки, подпись «проект». +- Живая проверка на стенде (perf-pg, форма 4): все сценарии спеки, две вкладки для рассылки. + +## Risks / Trade-offs + +- Перенумерация `sort_order` сервером при появлении/исчезновении ROOT — фронт сопоставляет агрегаты по id (`alignNodeRows`), но позиция вставки нового ITEM опирается на `sort_order`; при расхождении строка может встать не на своё место до перезагрузки. Митигация — позиция по `sort_order` соседей из ответа, тест на пустой раздел. +- Ветка поверх #304: если #304 изменится по ревью, нужен rebase. +- Excel: самостоятельный ITEM на уровне группировки 1 (вместо 2) — проверить вид, не ломать раскладку колонок по `depth`. + +## Оставлено исполнителю + +- Точная форма `sort_path` (D3) при соблюдении порядка и непересечения. +- Отдельный компонент окна переименования или переиспользование `CreateProgramModal`. +- Размещение функции подписи третьего столбца в конфиге колонок FORM_4. + +## Decisions made without discussion + +| Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---| +| 2026-11-08 | Ветка от `feat/303-aurora-1978-…` (PR #304), а не от `test` | удаление программ/проектов и `alignNodeRows` уже там; 1984 их использует | rebase при изменениях #304 по ревью | +| 2026-11-08 | «Добавить проект»: программа → в ней; раздел/ничего → в разделе; проект/расход → рядом (та же программа или раздел) | естественное продолжение текущей кнопки, без новой кнопки | при другом ожидании — правка выбора цели (одна функция) | +| 2026-11-08 | Одна функция `add_form4_project` с DEFAULT-параметрами вместо новой | старые вызовы работают, одна точка прав и аудита | DROP/CREATE функции в миграции | +| 2026-11-08 | Самостоятельный ITEM с `depth = 2`, без новой глубины | фронт и выгрузка строят дерево по `row_type`, вид как у проекта | отступ в Excel/на экране может потребовать правки | +| 2026-11-08 | Подпись «программа»/«проект» (1979) и в выгрузке Excel | выгрузка повторяет экран (как 1902/1903) | лишняя правка export_service, если заказчику нужен код | +| 2026-11-08 | Сортировка и подпись статей (1989) на клиенте | список короткий, сервер не трогаем | при большом списке — перенос на сервер | +| 2026-11-08 | Окно переименования закрывается по ответу сервера, сопоставление по событию и id | требование «при ошибке сохранять введённое значение» | при двух быстрых переименованиях одного узла — ответ может прийти «не тому» окну (маловероятно) | +| 2026-11-08 | Соединения уровня проекта в `v_form4_sheet_jsonb` — `IS NOT DISTINCT FROM` по `prog_id` (находка рецензента дизайна) | иначе суммы самостоятельного проекта теряются | при ошибке — изменение сумм существующих форм; закрывается тестом эквивалентности GET | +| 2026-11-08 | Ограничение `form_id IS NOT NULL` для узлов без родителя | самостоятельный проект без формы не должен существовать | миграция упадёт, если в данных есть программа без формы — проверка перед `ADD CONSTRAINT` | +| 2026-11-08 | Outline самостоятельного проекта в Excel — уровень программы (1), отступ — как у проекта | получается без правки писателя, «нет уровня программы» | если заказчик ждёт отступ как у программы — правка нормализатора | +| 2026-11-08 | Гонку одновременного создания не тестируем, позиция до перезагрузки может отличаться | сервер упорядочивает по id, редкий случай | редкое визуальное расхождение до F5 | diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/proposal.md b/openspec/changes/aurora-1984-pd-standalone-projects/proposal.md new file mode 100644 index 0000000..00d39bb --- /dev/null +++ b/openspec/changes/aurora-1984-pd-standalone-projects/proposal.md @@ -0,0 +1,42 @@ +# Proposal + +## Why + +На форме 4 (FORM_4, на фронте «Проектная деятельность») иерархия листа жёсткая: раздел (лист AHR/CAP/OPER) → программа → проект → расходы. Проект без программы создать нельзя: интерфейс требует выделить программу, `v3.add_form4_project` отвергает вызов без `p_program_id`, у проекта нет своего раздела (ограничение `chk_form4_project_section_code`), а лист находит проекты только через программу. Пользователю приходится заводить программу-обёртку ради одного проекта. + +Замечание тестировщика AURORA-1984 (Gitea #305): проект можно создать прямо в разделе, без программы. Человек (08.11.2026): «всё то же самое, как и сейчас, только нет уровня программы»; в разделе программы и самостоятельные проекты стоят вперемешку в порядке создания. + +В той же ветке закрываются три смежных замечания по ПД: AURORA-1979 (подпись «программа»/«проект» в третьем столбце), AURORA-1980 (переименование программы и проекта), AURORA-1989 (окно выбора расхода: номер статьи, сортировка, поиск). + +## What Changes + +- **Самостоятельный проект.** Запись `v3.form4_project` с `level='project'`, `parent_id IS NULL`, собственными `section_code` и `form_id`. Ограничение `chk_form4_project_section_code` ослабляется: раздел обязателен у программы и у проекта без родителя, запрещён у проекта в программе. +- **Создание.** `v3.add_form4_project` принимает либо программу (как сейчас), либо форму и раздел. WS-событие `project_added` принимает `program_id` необязательно; без него проект создаётся в разделе текущего листа соединения. +- **Лист.** `v3.v_form4_sheet_jsonb` выводит самостоятельный проект строкой ITEM прямо под ROOT раздела (без GROUP), с расходами под ним. Порядок на втором уровне — по id верхнего узла (программы или самостоятельного проекта), то есть в порядке создания. Суммы проекта входят в ROOT один раз; фиктивной GROUP с `program_id = NULL` нет. +- **Правка и удаление.** `v3.upd_form4_project` и `v3.del_form4_project` работают и без родителя: раздел берётся у проекта, в ответе нет GROUP. Запрет удалять проект с расходами — прежний. +- **Фронт.** «Добавить проект»: выделена программа → внутри неё; выделена строка раздела или ничего → в разделе; выделен проект или расход → рядом, в той же программе или в разделе. Вставка нового ITEM под ROOT. Удаление (AURORA-1978) уже работает по `project_id`. +- **AURORA-1979.** В третьем столбце листов ПД у программы «программа», у проекта (в программе и самостоятельного) «проект» вместо кода раздела; так же в выгрузке Excel. Коды статей у строк расходов и ROOT не меняются. +- **AURORA-1980.** Действие «Переименовать» для выделенной программы или проекта: окно с текущим названием, события `program_updated` / `project_updated` (сервер уже поддерживает). Пустое название и пробелы не сохраняются; при ошибке окно остаётся открытым с введённым текстом. +- **AURORA-1989.** Окно выбора расхода: «номер статьи — название», сортировка по номеру статьи с учётом числовых частей (2 раньше 10), поиск по номеру и названию. +- Новая миграция `0059_form4_standalone_project` (номер — следующий после 0058 на момент ветки). + +**Не меняется:** +- существующие данные: все проекты остаются в своих программах, перенос проекта в программу и из неё не делается; +- своды, листы SMETA и STRUCTURE (не читают программы и проекты); +- права: создание, правка и удаление самостоятельного проекта проверяются так же, как у программы (`v3.can_edit` по форме и разделу); +- контракт строк листа для программ и проектов в программах. + +## Capabilities + +### New Capabilities +- `form4-programs-projects`: программы и проекты листов AHR/CAP/OPER формы 4 — создание (в программе и в разделе), переименование, удаление, место в листе и в итогах, подпись типа в третьем столбце, выбор расхода для проекта. + +### Modified Capabilities +Нет. + +## Impact + +- SQL: миграция `0059_*.py` + `sql/0059_*.sql`: ограничение `form4_project`, `v3.add_form4_project` (новая сигнатура — DROP/CREATE), `v3.upd_form4_project`, `v3.del_form4_project`, `v3.v_form4_sheet_jsonb` (тело — от версии 0053). +- Python: `websocket.py` (`__add_project`), `SheetService.add_project` (проверка доступа по форме без программы), `SheetRepository.add_project`; при необходимости — export_service (подпись «программа»/«проект» в третьем столбце). +- Фронт: `RealtimeTable.jsx`, `RealtimeContext.js`, `useRealtimeData.js`, `utils/cellUtils.js`, модалки создания/переименования, `SelectExpenseItemModal.jsx`, конфиг колонок FORM_4. +- Тесты: интеграционные (лист, итоги, создание/правка/удаление самостоятельного проекта, ответ `_form_affected_rows`), unit фронта (`node --test`), живая проверка на стенде. diff --git a/openspec/changes/aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md b/openspec/changes/aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md new file mode 100644 index 0000000..34e8b74 --- /dev/null +++ b/openspec/changes/aurora-1984-pd-standalone-projects/specs/form4-programs-projects/spec.md @@ -0,0 +1,92 @@ +# Spec Delta + +## Purpose + +Программы и проекты на листах AHR/CAP/OPER формы 4 (FORM_4, «Проектная деятельность»): как они создаются, переименовываются и удаляются, где стоят в листе, как участвуют в итогах и выгрузке, как подписаны и как к проекту добавляется расход. + +## ADDED Requirements + +### Requirement: Проект создаётся в программе или прямо в разделе +Пользователь с правом добавлять строки в раздел листа SHALL иметь возможность создать проект как внутри программы этого раздела, так и прямо в разделе, без программы. Название SHALL быть непустым после обрезки пробелов. Права SHALL проверяться так же, как при создании программы в этом разделе. + +#### Scenario: Проект в разделе +- **WHEN** на листе CAP выделена строка раздела (или ничего не выделено) и пользователь создаёт проект «П1» +- **THEN** «П1» появляется в разделе CAP без программы у всех подключённых к листу пользователей без перезагрузки +- **AND** после перезагрузки страницы «П1» стоит на том же месте + +#### Scenario: Проект в программе — как раньше +- **WHEN** выделена программа «Пр» и пользователь создаёт проект +- **THEN** проект создаётся внутри «Пр» + +#### Scenario: Выделен проект или расход +- **WHEN** выделен проект или строка расхода и пользователь создаёт проект +- **THEN** новый проект создаётся там же, где выделенный проект: в той же программе или прямо в разделе + +#### Scenario: Пустое название +- **WHEN** пользователь вводит название из одних пробелов +- **THEN** проект не создаётся + +### Requirement: Место самостоятельного проекта в листе +Самостоятельный проект SHALL выводиться строкой ITEM непосредственно под ROOT своего раздела, без строки GROUP над ним; его расходы — строками INPUT под ним. На уровне под ROOT программы и самостоятельные проекты SHALL идти в порядке создания (по id записи `form4_project`). Проекты внутри программы и их расходы SHALL выводиться как раньше. + +#### Scenario: Вперемешку в порядке создания +- **WHEN** в разделе последовательно созданы программа «А», самостоятельный проект «Б», программа «В» +- **THEN** под ROOT раздела строки идут в порядке «А» (GROUP), «Б» (ITEM), «В» (GROUP) + +#### Scenario: Пустой раздел +- **WHEN** в разделе нет ни одного расхода, но есть самостоятельный проект +- **THEN** проект виден в листе так же, как видна пустая программа + +### Requirement: Суммы самостоятельного проекта +Суммы расходов самостоятельного проекта SHALL входить в строку ITEM проекта и в ROOT раздела ровно один раз. Лист SHALL NOT содержать строку GROUP без программы (`program_id` пустой). Ответ на правку ячейки расхода самостоятельного проекта SHALL содержать строку расхода, ITEM проекта и ROOT раздела. + +#### Scenario: Итог раздела +- **WHEN** в разделе есть программа с расходом 100 и самостоятельный проект с расходом 50 в `plan.q1` +- **THEN** ROOT раздела показывает 150 в `plan.q1`, ITEM самостоятельного проекта — 50 + +#### Scenario: Правка расхода +- **WHEN** пользователь меняет `plan.q1` у расхода самостоятельного проекта +- **THEN** ответ содержит этот расход, ITEM проекта и ROOT раздела с пересчитанными суммами и не содержит GROUP + +### Requirement: Расходы самостоятельного проекта +К самостоятельному проекту SHALL можно добавлять и удалять расходы так же, как к проекту в программе. + +#### Scenario: Добавление расхода +- **WHEN** выделен самостоятельный проект и пользователь добавляет расход по статье +- **THEN** расход появляется под этим проектом, суммы проекта и раздела пересчитываются + +### Requirement: Переименование программы и проекта +Пользователь с правом изменения SHALL иметь возможность переименовать выделенную программу или проект (в программе и самостоятельный). Окно переименования SHALL открываться с текущим названием. Пустое название и название из одних пробелов SHALL NOT сохраняться. При ошибке сервера окно SHALL оставаться открытым с введённым текстом и показывать понятное сообщение. Отмена SHALL оставлять прежнее название. Переименование SHALL NOT менять состав проектов, расходов и суммы. + +#### Scenario: Успешное переименование +- **WHEN** пользователь переименовывает программу «А» в «А2» +- **THEN** у всех подключённых пользователей строка показывает «А2», после перезагрузки тоже + +#### Scenario: Ошибка +- **WHEN** сервер отказывает в переименовании (например, нет прав) +- **THEN** окно остаётся открытым с введённым названием и показывает причину + +### Requirement: Удаление самостоятельного проекта +Пустой самостоятельный проект SHALL удаляться с подтверждением, как проект в программе (AURORA-1978). Проект с расходами SHALL NOT удаляться, пользователь видит причину. + +#### Scenario: Удаление пустого +- **WHEN** пользователь удаляет пустой самостоятельный проект и подтверждает +- **THEN** проект исчезает из листа у всех подключённых, ROOT раздела пересчитывается + +### Requirement: Подпись типа в третьем столбце +В третьем столбце листов AHR/CAP/OPER формы 4 строка программы SHALL быть подписана «программа», строка проекта (в программе и самостоятельного) — «проект». Строки ROOT и расходов SHALL показывать код, как раньше. Подпись SHALL совпадать на экране и в выгрузке Excel. + +#### Scenario: Подписи +- **WHEN** открыт лист CAP с программой, проектом в ней и самостоятельным проектом +- **THEN** напротив программы «программа», напротив обоих проектов «проект», у расходов — код статьи + +### Requirement: Выбор расхода для проекта +Окно выбора расхода SHALL показывать варианты в виде «<номер статьи> — <название>», упорядоченные по номеру статьи с учётом числовых частей (например, 2 раньше 10). Поиск SHALL находить вариант по номеру и по названию. Выбор SHALL добавлять расход именно по выбранной статье. + +#### Scenario: Сортировка +- **WHEN** среди статей есть номера «3.2.» и «3.10.» +- **THEN** «3.2.» стоит выше «3.10.» + +#### Scenario: Поиск по номеру +- **WHEN** пользователь вводит «3.01» +- **THEN** в списке остаются статьи, номер которых содержит «3.01»