AURORA-1984: создание, переименование и удаление проекта без программы в SQL
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
836e812bad
commit
209d36e832
@ -17,8 +17,196 @@ ALTER TABLE v3.form4_project ADD CONSTRAINT chk_form4_project_section_code
|
|||||||
ALTER TABLE v3.form4_project ADD CONSTRAINT chk_form4_project_standalone_form
|
ALTER TABLE v3.form4_project ADD CONSTRAINT chk_form4_project_standalone_form
|
||||||
CHECK (parent_id IS NOT NULL OR form_id IS NOT NULL);
|
CHECK (parent_id IS NOT NULL OR form_id IS NOT NULL);
|
||||||
|
|
||||||
-- ═══ 2. Функции add/upd/del_form4_project (Task 2) ═══════════════════════════
|
-- ═══ 2. Функции add/upd/del_form4_project ════════════════════════════════════
|
||||||
-- >>> здесь будут add_form4_project / upd_form4_project / del_form4_project <<<
|
-- Права: создание — v3.can_edit; переименование и удаление — как раньше (R-6).
|
||||||
|
DROP FUNCTION v3.add_form4_project(varchar, int4);
|
||||||
|
|
||||||
|
CREATE FUNCTION 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
|
||||||
|
)
|
||||||
|
RETURNS TABLE(project_id integer, row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_section_code VARCHAR;
|
||||||
|
v_form_id INT;
|
||||||
|
v_user_id INT;
|
||||||
|
v_decision JSONB;
|
||||||
|
v_msg TEXT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF NOT ((p_program_id IS NOT NULL AND p_form_id IS NULL AND p_section_code IS NULL)
|
||||||
|
OR (p_program_id IS NULL AND p_form_id IS NOT NULL AND p_section_code IS NOT NULL)) THEN
|
||||||
|
RAISE EXCEPTION 'Укажите либо программу, либо форму и раздел';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_program_id IS NOT NULL THEN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_program_id AND fp.level = 'program') THEN
|
||||||
|
RAISE EXCEPTION 'program id=% не существует или не имеет level=program', p_program_id;
|
||||||
|
END IF;
|
||||||
|
SELECT fp.form_id, fp.section_code INTO v_form_id, v_section_code
|
||||||
|
FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_program_id AND fp.level = 'program';
|
||||||
|
ELSE
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.budget_form bf
|
||||||
|
WHERE bf.form_type_code = 'FORM_4' AND bf.id = p_form_id) THEN
|
||||||
|
RAISE EXCEPTION 'Только FORM_4';
|
||||||
|
END IF;
|
||||||
|
IF p_section_code NOT IN ('AHR', 'CAP', 'OPER') THEN
|
||||||
|
RAISE EXCEPTION 'Раздел % недоступен для проекта', p_section_code;
|
||||||
|
END IF;
|
||||||
|
v_form_id := p_form_id;
|
||||||
|
v_section_code := p_section_code;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_user_id := v3.current_user_id();
|
||||||
|
v_decision := v3.can_edit(v_form_id, v_section_code, NULL, NULL, v_user_id);
|
||||||
|
|
||||||
|
IF NOT (v_decision->>'ok')::boolean THEN
|
||||||
|
IF v_decision->>'code' = 'role_not_allowed' THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: Невозможно вставить строку';
|
||||||
|
|
||||||
|
ELSIF v_decision->>'code' = 'org_not_assigned' THEN
|
||||||
|
RAISE EXCEPTION 'org_not_assigned: Невозможно вставить строку';
|
||||||
|
|
||||||
|
ELSIF v_decision->>'code' = 'window_closed' THEN
|
||||||
|
IF v_decision ? 'opens_at' THEN
|
||||||
|
v_msg := format('window_closed: opens %s', v_decision->>'opens_at');
|
||||||
|
ELSIF v_decision->>'closes_at' IS NOT NULL THEN
|
||||||
|
v_msg := format('window_closed: closed %s', v_decision->>'closes_at');
|
||||||
|
ELSE
|
||||||
|
v_msg := 'window_closed: no phase';
|
||||||
|
END IF;
|
||||||
|
RAISE EXCEPTION '%', v_msg;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '%', v_decision->>'code';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_program_id IS NOT NULL THEN
|
||||||
|
INSERT INTO v3.form4_project (name, level, parent_id, form_id)
|
||||||
|
VALUES (p_name, 'project', p_program_id, v_form_id)
|
||||||
|
RETURNING id INTO v_pid;
|
||||||
|
ELSE
|
||||||
|
INSERT INTO v3.form4_project (name, level, parent_id, form_id, section_code)
|
||||||
|
VALUES (p_name, 'project', NULL, v_form_id, v_section_code)
|
||||||
|
RETURNING id INTO v_pid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'level', 'project',
|
||||||
|
'parent_id', p_program_id,
|
||||||
|
'form_id', v_form_id,
|
||||||
|
'section_code', v_section_code,
|
||||||
|
'form_type', 'FORM_4'
|
||||||
|
)),
|
||||||
|
v_form_id, NULL, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
IF v_section_code IS NOT NULL THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form4_sheet_jsonb(v_form_id, v_section_code, NULL) v
|
||||||
|
WHERE (v.row_type = 'ITEM' AND (v.data->'header'->>'project_id')::INT = v_pid)
|
||||||
|
OR (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = p_program_id)
|
||||||
|
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = v_section_code)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form4_project(p_project_id INT, p_name VARCHAR)
|
||||||
|
RETURNS TABLE (project_id INT, row_type VARCHAR, depth INT, sort_order BIGINT, data JSONB)
|
||||||
|
AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_project v3.form4_project%ROWTYPE;
|
||||||
|
v_program v3.form4_project%ROWTYPE;
|
||||||
|
v_section_code VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO v_project FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_project_id AND fp.level = 'project';
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'Проект id=% не найден', p_project_id;
|
||||||
|
END IF;
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT * INTO v_program FROM v3.form4_project fp WHERE fp.id = v_project.parent_id;
|
||||||
|
v_section_code := COALESCE(v_program.section_code, v_project.section_code);
|
||||||
|
UPDATE v3.form4_project SET name = trim(p_name) WHERE id = p_project_id;
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_UPDATE', 'PROJECT',
|
||||||
|
jsonb_build_object('project_id', p_project_id, 'name', trim(p_name),
|
||||||
|
'level', 'project', 'parent_id', v_project.parent_id,
|
||||||
|
'form_id', v_project.form_id, 'form_type', 'FORM_4'),
|
||||||
|
v_project.form_id, NULL, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT p_project_id, v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form4_sheet_jsonb(v_project.form_id, v_section_code, NULL) v
|
||||||
|
WHERE (v.row_type = 'ITEM' AND (v.data->'header'->>'project_id')::INT = p_project_id)
|
||||||
|
OR (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = v_project.parent_id)
|
||||||
|
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = v_section_code)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.del_form4_project(p_project_id INT)
|
||||||
|
RETURNS TABLE (project_id INT, row_type VARCHAR, depth INT, sort_order BIGINT, data JSONB)
|
||||||
|
AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_project v3.form4_project%ROWTYPE;
|
||||||
|
v_program v3.form4_project%ROWTYPE;
|
||||||
|
v_section_code VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO v_project FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_project_id AND fp.level = 'project';
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'Проект id=% не найден', p_project_id;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (SELECT 1 FROM v3.budget_line bl WHERE bl.project_id = p_project_id) THEN
|
||||||
|
RAISE EXCEPTION 'Нельзя удалить проект, содержащий строки бюджета';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT * INTO v_program FROM v3.form4_project fp WHERE fp.id = v_project.parent_id;
|
||||||
|
v_section_code := COALESCE(v_program.section_code, v_project.section_code);
|
||||||
|
DELETE FROM v3.form4_project WHERE id = p_project_id;
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_DELETE', 'PROJECT',
|
||||||
|
jsonb_build_object('project_id', p_project_id, 'name', v_project.name,
|
||||||
|
'level', 'project', 'parent_id', v_project.parent_id,
|
||||||
|
'form_id', v_project.form_id, 'form_type', 'FORM_4'),
|
||||||
|
v_project.form_id, NULL, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT p_project_id, v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form4_sheet_jsonb(v_project.form_id, v_section_code, NULL) v
|
||||||
|
WHERE (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = v_project.parent_id)
|
||||||
|
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = v_section_code)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
-- ═══ 3. Лист ═════════════════════════════════════════════════════════════════
|
-- ═══ 3. Лист ═════════════════════════════════════════════════════════════════
|
||||||
-- Тело — копия sql/0053_form2_ahr_source_sheet.sql, изменены: all_projects (+самостоятельные),
|
-- Тело — копия sql/0053_form2_ahr_source_sheet.sql, изменены: all_projects (+самостоятельные),
|
||||||
|
|||||||
@ -14,12 +14,15 @@ def f4(client, db_session, admin_tokens, auth_headers):
|
|||||||
return F4(client, db_session, auth_headers(admin_tokens))
|
return F4(client, db_session, auth_headers(admin_tokens))
|
||||||
|
|
||||||
|
|
||||||
def standalone(f4, name="solo", section="CAP"):
|
def add(f4, name="solo", section="CAP"):
|
||||||
return f4.sql(
|
return f4.sql(
|
||||||
"INSERT INTO v3.form4_project (name, level, form_id, section_code) "
|
"SELECT project_id, row_type FROM v3.add_form4_project(:n, NULL, :f, :s)",
|
||||||
"VALUES (:n, 'project', :f, :s) RETURNING id",
|
|
||||||
n=name, f=f4.form_id, s=section,
|
n=name, f=f4.form_id, s=section,
|
||||||
)[0][0]
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def standalone(f4, name="solo", section="CAP"):
|
||||||
|
return add(f4, name, section)[0][0]
|
||||||
|
|
||||||
|
|
||||||
def outline(rows):
|
def outline(rows):
|
||||||
@ -78,6 +81,7 @@ def test_cell_patch_response(f4, client):
|
|||||||
assert resp.status_code == 200, resp.text
|
assert resp.status_code == 200, resp.text
|
||||||
by_type = {r["row_type"]: r["data"] for r in _loads(resp.text)["result"]}
|
by_type = {r["row_type"]: r["data"] for r in _loads(resp.text)["result"]}
|
||||||
assert {"INPUT", "ITEM", "ROOT"} <= set(by_type) and "GROUP" not in by_type
|
assert {"INPUT", "ITEM", "ROOT"} <= set(by_type) and "GROUP" not in by_type
|
||||||
|
assert Decimal(str(by_type["ITEM"]["plan"]["q1"])) == 20 # значение дошло до ITEM (T1.1)
|
||||||
fresh = next(r for r in f4.sheet() if r["row_type"] == "ITEM")
|
fresh = next(r for r in f4.sheet() if r["row_type"] == "ITEM")
|
||||||
assert strip_nulls(by_type["ITEM"]["plan"]) == fresh["data"]["plan"] # GET без null (AURORA-1848)
|
assert strip_nulls(by_type["ITEM"]["plan"]) == fresh["data"]["plan"] # GET без null (AURORA-1848)
|
||||||
|
|
||||||
@ -110,3 +114,58 @@ def test_sheet_without_standalone_equals_0053(f4):
|
|||||||
old = [tuple(r) for r in f4.sql(q.format("pg_temp.v_form4_sheet_old"), f=f4.form_id)]
|
old = [tuple(r) for r in f4.sql(q.format("pg_temp.v_form4_sheet_old"), f=f4.form_id)]
|
||||||
new = [tuple(r) for r in f4.sql(q.format("v3.v_form4_sheet_jsonb"), f=f4.form_id)]
|
new = [tuple(r) for r in f4.sql(q.format("v3.v_form4_sheet_jsonb"), f=f4.form_id)]
|
||||||
assert new == old
|
assert new == old
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_in_empty_section_returns_item_only(f4):
|
||||||
|
assert [r[1] for r in add(f4)] == ["ITEM"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_with_root_returns_item_and_root(f4):
|
||||||
|
f4.line(f4.project(f4.program()))
|
||||||
|
assert sorted(r[1] for r in add(f4)) == ["ITEM", "ROOT"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_rejects_blank_name(f4):
|
||||||
|
with pytest.raises(Exception, match="p_name обязателен"):
|
||||||
|
add(f4, name=" ")
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_rejects_no_target(f4):
|
||||||
|
with pytest.raises(Exception, match="Укажите либо программу, либо форму и раздел"):
|
||||||
|
f4.sql("SELECT * FROM v3.add_form4_project('x')")
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_rejects_both_targets(f4):
|
||||||
|
prog = f4.program()
|
||||||
|
with pytest.raises(Exception, match="Укажите либо программу, либо форму и раздел"):
|
||||||
|
f4.sql("SELECT * FROM v3.add_form4_project('x', :p, :f, 'CAP')", p=prog, f=f4.form_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_rejects_foreign_section(f4):
|
||||||
|
with pytest.raises(Exception, match="Раздел SMETA недоступен для проекта"):
|
||||||
|
add(f4, section="SMETA")
|
||||||
|
|
||||||
|
|
||||||
|
def test_two_argument_call_still_creates_in_program(f4):
|
||||||
|
prog = f4.program(); prj = f4.project(prog)
|
||||||
|
item = next(r for r in f4.sheet() if r["row_type"] == "ITEM")
|
||||||
|
assert (item["data"]["header"]["program_id"], item["data"]["header"]["project_id"]) == (prog, prj)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_standalone(f4):
|
||||||
|
solo = standalone(f4)
|
||||||
|
rows = f4.sql("SELECT row_type, data::text FROM v3.upd_form4_project(:p, ' Новое ')", p=solo)
|
||||||
|
assert [r[0] for r in rows] == ["ITEM"]
|
||||||
|
assert _loads(rows[0][1])["header"]["name"] == "Новое"
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_empty_standalone_in_empty_section_returns_nothing(f4):
|
||||||
|
solo = standalone(f4)
|
||||||
|
assert f4.sql("SELECT * FROM v3.del_form4_project(:p)", p=solo) == []
|
||||||
|
assert outline(f4.sheet()) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_standalone_with_line_rejected(f4):
|
||||||
|
solo = standalone(f4); f4.line(solo)
|
||||||
|
with pytest.raises(Exception, match="Нельзя удалить проект, содержащий строки бюджета"):
|
||||||
|
f4.sql("SELECT * FROM v3.del_form4_project(:p)", p=solo)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user