AURORA-1984: проекты ПД без программы (+1979, 1980, 1989) #306
@ -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. Ограничения ══════════════════════════════════════════════════════════
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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` общий для всех форм, его правка задела бы их выгрузки | лишний класс из нескольких строк |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user