AURORA-1984: правки по итоговому ревью (M-5, M-6, M-7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-08 23:16:20 +04:00
parent 77db9cb02f
commit e5660add7a
4 changed files with 13 additions and 14 deletions

View File

@ -1,5 +1,6 @@
-- AURORA-1984: проект без программы (самостоятельный проект формы 4).
-- Часть 1: ограничения модели и лист v3.v_form4_sheet_jsonb.
-- Весь файл: ограничения модели, v3.add_form4_project / upd_form4_project / del_form4_project
-- и лист v3.v_form4_sheet_jsonb.
-- Самостоятельный проект: level='project', parent_id IS NULL, section_code и form_id заданы.
-- ═══ 1. Ограничения ══════════════════════════════════════════════════════════

View File

@ -1,3 +1,4 @@
import copy
import pytest
from src.services.export_service.export_normalizers import (
@ -91,6 +92,7 @@ def test_form3_depth_normalizer_flattens_nested_data():
({"line_id": 9, "header": {"section_code": "3.01.", "program_id": 1, "project_id": 2}}, "3.01."),
])
def test_form4_depth_normalizer_type_label(data, expected):
original = copy.deepcopy(data)
normalized = Form4DepthRowNormalizer(ExcelValueSerializer()).normalize(data)
assert normalized["header.section_code"] == expected
assert data["header"]["section_code"] != "программа"
assert data == original

View File

@ -234,21 +234,16 @@ def test_export_service_uses_sheet_writer_strategy():
assert smeta_writer.calls == 1
def test_standalone_project_outline_without_group():
rows = [("ROOT", 0, 1, {}), ("ITEM", 2, 2, {}), ("INPUT", 3, 3, {}), ("GROUP", 1, 4, {})]
assert [(row[0], level) for row, level in _hierarchical_rows(rows)] == [
("ROOT", 0), ("ITEM", 1), ("INPUT", 2), ("GROUP", 1),
]
def test_standalone_project_after_program_is_sibling_of_program():
"""AURORA-1984 I-1: самостоятельный проект после программы — уровень программы."""
"""AURORA-1984 I-1: самостоятельный проект (ITEM без program_id) — уровень программы, без GROUP и после программы."""
def node(kind, **header):
return (kind, 0, 0, {"header": header, "line_id": None})
rows = [
node("ROOT"),
node("ITEM", project_id=9),
("INPUT", 4, 0, {"header": {"project_id": 9}, "line_id": 4}),
node("GROUP", program_id=1),
node("ITEM", project_id=10, program_id=1),
("INPUT", 4, 0, {"header": {"project_id": 10, "program_id": 1}, "line_id": 5}),
@ -258,4 +253,4 @@ def test_standalone_project_after_program_is_sibling_of_program():
node("ITEM", project_id=12),
("INPUT", 4, 0, {"header": {"project_id": 12}, "line_id": 7}),
]
assert [level for _, level in _hierarchical_rows(rows)] == [0, 1, 2, 3, 1, 2, 1, 1, 2]
assert [level for _, level in _hierarchical_rows(rows)] == [0, 1, 2, 1, 2, 3, 1, 2, 1, 1, 2]

View File

@ -38,7 +38,7 @@
`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`. ORM-модель `api/src/db/models/form4_project.py` повторяет ограничения — правится вместе с миграцией.
### 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 раздела (если он есть в листе).
Новая сигнатура: `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` с `section_code` (ключ `parent_id` в событии отсутствует: `log_event` применяет `jsonb_strip_nulls`). Возвращает строки так же, как вариант с программой: новый ITEM + ROOT раздела (если он есть в листе).
### D3. Лист: ITEM под ROOT, порядок по id верхнего узла
В `v3.v_form4_sheet_jsonb`:
@ -104,6 +104,7 @@ ROOT выводится только при наличии расходов в
- Ветка поверх #304: если #304 изменится по ревью, нужен rebase.
- Excel: самостоятельный ITEM на уровне группировки 1 (вместо 2) — проверить вид, не ломать раскладку колонок по `depth`.
- `rowUtils.getParentId` для ROOT и GROUP возвращает `expense_item_id`, и `insertCell` сравнивает его с `project_id`: при совпадении чисел новый расход встанет под ROOT до перезагрузки. Поведение существовало и для проектов в программах; в этой ветке не меняется (O-1).
- Исторический расход без проекта (`project_id` NULL, sort 999999), оказавшийся сразу после самостоятельного проекта, построители дерева (фронт `buildRowHierarchy` и выгрузка) прикрепят к нему — тот же класс дефекта, что уже есть у последнего проекта программы. Follow-up, если в реальных данных есть такие строки (M-1 итогового ревью).
- GET срезает null (AURORA-1848), поэтому `header.program_id` приходит то `null`, то `undefined`: сравнивать только через `!= null`.
## Оставлено исполнителю
@ -117,13 +118,13 @@ ROOT выводится только при наличии расходов в
| 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 | Самостоятельный ITEM с `depth = 2`, без новой глубины | раскраска и вид — по `row_type`, как у проекта; уровень в дереве задаёт явный признак самостоятельного проекта (`isStandaloneProject` / `_is_standalone_project`, R-8, design #20), `depth = 2` остаётся в данных для `findRowPath` | отступ в 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 и отступ самостоятельного проекта — уровень программы (1), на экране и в Excel | получается без правки писателя и `Cell.jsx`, «нет уровня программы» | если заказчик ждёт отступ как у проекта — правка отображения |
| 2026-11-08 | Outline и отступ самостоятельного проекта — уровень программы (1), на экране и в Excel | уровень 1 задаёт явный признак самостоятельного проекта в построителях дерева (фронт `buildRowHierarchy`) и выгрузки (`_hierarchical_rows`), см. R-8 (design #20); писатель и `Cell.jsx` не менялись, «нет уровня программы» | если заказчик ждёт отступ как у проекта — правка отображения |
| 2026-11-08 | Гонку одновременного создания не тестируем, позиция до перезагрузки может отличаться | сервер упорядочивает по id, редкий случай | редкое визуальное расхождение до F5 |
| 2026-11-08 | R-1 (design #13): подпись третьего столбца — через строковый признак в конфиге и `accessorFn` в `processColumns` | `structuredClone` конфига падает на функциях | правка одной ветки в `processColumns` |
| 2026-11-08 | R-2 (design #14): отдельный `Form4DepthRowNormalizer` для AHR/CAP/OPER формы 4 | `Form1DepthRowNormalizer` общий для всех форм, его правка задела бы их выгрузки | лишний класс из нескольких строк |