Merge pull request 'remove-programs: обновление и удаление програм и проектов четвертой формы' (#133) from remove-programs into test

Reviewed-on: #133
This commit is contained in:
tsygankoviva 2026-08-31 11:46:43 +03:00
commit dcbd133abd
5 changed files with 413 additions and 2 deletions

View File

@ -0,0 +1,14 @@
from sql_migration import run_sql_file_migration
revision = "0024"
down_revision = "0023"
branch_labels = None
depends_on = None
def upgrade() -> None:
run_sql_file_migration("0024_upd_form4_projects.sql")
def downgrade() -> None:
pass

View File

@ -0,0 +1,155 @@
DROP FUNCTION IF EXISTS v3.upd_form4_program(INT, VARCHAR, BOOLEAN);
DROP FUNCTION IF EXISTS v3.upd_form4_program(INT, VARCHAR);
CREATE FUNCTION v3.upd_form4_program(p_program_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_program v3.form4_project%ROWTYPE;
BEGIN
SELECT * INTO v_program FROM v3.form4_project fp
WHERE fp.id = p_program_id AND fp.level = 'program';
IF NOT FOUND THEN
RAISE EXCEPTION 'Программа id=% не найдена', p_program_id;
END IF;
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
RAISE EXCEPTION 'p_name обязателен';
END IF;
UPDATE v3.form4_project SET name = trim(p_name) WHERE id = p_program_id;
PERFORM v3.log_event(
'PROJECT_UPDATE', 'PROJECT',
jsonb_build_object('project_id', p_program_id, 'name', trim(p_name),
'level', 'program', 'form_id', v_program.form_id, 'form_type', 'FORM_4'),
v_program.form_id, NULL, NULL
);
RETURN QUERY
SELECT p_program_id, v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form4_sheet_jsonb(v_program.form_id, v_program.section_code, NULL) v
WHERE (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_program.section_code)
ORDER BY v.sort_order;
END;
$function$
LANGUAGE plpgsql VOLATILE;
DROP FUNCTION IF EXISTS v3.del_form4_program(INT);
CREATE FUNCTION v3.del_form4_program(p_program_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_program v3.form4_project%ROWTYPE;
BEGIN
SELECT * INTO v_program FROM v3.form4_project fp
WHERE fp.id = p_program_id AND fp.level = 'program';
IF NOT FOUND THEN
RAISE EXCEPTION 'Программа id=% не найдена', p_program_id;
END IF;
IF EXISTS (SELECT 1 FROM v3.form4_project fp WHERE fp.parent_id = p_program_id) THEN
RAISE EXCEPTION 'Нельзя удалить программу, содержащую проекты';
END IF;
DELETE FROM v3.form4_project WHERE id = p_program_id;
PERFORM v3.log_event(
'PROJECT_DELETE', 'PROJECT',
jsonb_build_object('project_id', p_program_id, 'name', v_program.name,
'level', 'program', 'form_id', v_program.form_id, 'form_type', 'FORM_4'),
v_program.form_id, NULL, NULL
);
RETURN QUERY
SELECT p_program_id, v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form4_sheet_jsonb(v_program.form_id, v_program.section_code, NULL) v
WHERE v.row_type = 'ROOT'
AND v.data->'header'->>'section_code' = v_program.section_code
ORDER BY v.sort_order;
END;
$function$
LANGUAGE plpgsql VOLATILE;
DROP FUNCTION IF EXISTS v3.upd_form4_project(INT, VARCHAR, BOOLEAN);
DROP FUNCTION IF EXISTS v3.upd_form4_project(INT, VARCHAR);
CREATE 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;
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;
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_program.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_program.section_code)
ORDER BY v.sort_order;
END;
$function$
LANGUAGE plpgsql VOLATILE;
DROP FUNCTION IF EXISTS v3.del_form4_project(INT);
CREATE 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;
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;
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_program.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_program.section_code)
ORDER BY v.sort_order;
END;
$function$
LANGUAGE plpgsql VOLATILE;

View File

@ -389,6 +389,26 @@ class FormEventProcess:
event_data=event_data["data"], event_data=event_data["data"],
user=curr_user, user=curr_user,
) )
case "program_updated":
return await self.__update_form4_program(
event_data=event_data["data"],
user=curr_user,
)
case "program_deleted":
return await self.__delete_form4_program(
event_data=event_data["data"],
user=curr_user,
)
case "project_updated":
return await self.__update_form4_project(
event_data=event_data["data"],
user=curr_user,
)
case "project_deleted":
return await self.__delete_form4_project(
event_data=event_data["data"],
user=curr_user,
)
case _: case _:
pass pass
@ -511,6 +531,48 @@ class FormEventProcess:
user=user, user=user,
) )
async def __update_form4_program(
self,
event_data: dict,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
return await self.sheet_service.update_form4_program(
program_id=event_data["program_id"],
name=event_data["name"],
user=user,
)
async def __delete_form4_program(
self,
event_data: dict,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
return await self.sheet_service.delete_form4_program(
program_id=event_data["program_id"],
user=user,
)
async def __update_form4_project(
self,
event_data: dict,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
return await self.sheet_service.update_form4_project(
project_id=event_data["project_id"],
name=event_data["name"],
user=user,
)
async def __delete_form4_project(
self,
event_data: dict,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
return await self.sheet_service.delete_form4_project(
project_id=event_data["project_id"],
user=user,
)
async def __del_row( async def __del_row(
self, self,
event_data: dict, event_data: dict,
@ -874,6 +936,10 @@ async def process_websocket(
"row_added", "row_added",
"program_added", "program_added",
"project_added", "project_added",
"program_updated",
"project_updated",
"program_deleted",
"project_deleted",
"row_deleted", "row_deleted",
]: ]:
await manager.broadcast_to_all( await manager.broadcast_to_all(

View File

@ -213,6 +213,92 @@ class SheetRepository:
).all() ).all()
] ]
async def update_form4_program(
self,
program_id: int,
name: str,
) -> list[tuple]:
return [
tuple(r) for r in (
await self.db.execute(
text(
"""
SELECT project_id, row_type, depth, sort_order, data
FROM v3.upd_form4_program(
:p_program_id,
:p_name
)
"""
),
{
"p_program_id": program_id,
"p_name": name,
},
)
).all()
]
async def delete_form4_program(
self,
program_id: int,
) -> list[tuple]:
return [
tuple(r) for r in (
await self.db.execute(
text(
"""
SELECT project_id, row_type, depth, sort_order, data
FROM v3.del_form4_program(:p_program_id)
"""
),
{"p_program_id": program_id},
)
).all()
]
async def update_form4_project(
self,
project_id: int,
name: str,
) -> list[tuple]:
return [
tuple(r) for r in (
await self.db.execute(
text(
"""
SELECT project_id, row_type, depth, sort_order, data
FROM v3.upd_form4_project(
:p_project_id,
:p_name
)
"""
),
{
"p_project_id": project_id,
"p_name": name,
},
)
).all()
]
async def delete_form4_project(
self,
project_id: int,
) -> list[tuple]:
return [
tuple(r) for r in (
await self.db.execute(
text(
"""
SELECT project_id, row_type, depth, sort_order, data
FROM v3.del_form4_project(:p_project_id)
"""
),
{"p_project_id": project_id},
)
).all()
]
async def add_line( async def add_line(
self, self,
form_id: int, form_id: int,
@ -294,4 +380,3 @@ class SheetRepository:
) )
).all() ).all()
] ]

View File

@ -170,6 +170,97 @@ class SheetService:
return (result[0][0], [tuple(r[1:]) for r in result]) return (result[0][0], [tuple(r[1:]) for r in result])
return None return None
async def update_form4_program(
self,
program_id: int,
name: str,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
await self._validate_form4_node_access(
node_type="program",
node_id=program_id,
user=user,
)
result = await self.sheet_repo.update_form4_program(
program_id=program_id,
name=name,
)
return (program_id, [tuple(r[1:]) for r in result])
async def delete_form4_program(
self,
program_id: int,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
await self._validate_form4_node_access(
node_type="program",
node_id=program_id,
user=user,
)
result = await self.sheet_repo.delete_form4_program(program_id=program_id)
return (program_id, [tuple(r[1:]) for r in result])
async def update_form4_project(
self,
project_id: int,
name: str,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
await self._validate_form4_node_access(
node_type="project",
node_id=project_id,
user=user,
)
result = await self.sheet_repo.update_form4_project(
project_id=project_id,
name=name,
)
return (project_id, [tuple(r[1:]) for r in result])
async def delete_form4_project(
self,
project_id: int,
user: AppUser,
) -> tuple[int, list[tuple]] | None:
await self._validate_form4_node_access(
node_type="project",
node_id=project_id,
user=user,
)
result = await self.sheet_repo.delete_form4_project(project_id=project_id)
return (project_id, [tuple(r[1:]) for r in result])
async def _validate_form4_node_access(
self,
node_type: str,
node_id: int,
user: AppUser,
) -> None:
if node_type not in {"program", "project"}:
raise SheetValidationError("Некорректный тип узла")
level = (
Form4ProjectLevelEnum.PROGRAM
if node_type == "program"
else Form4ProjectLevelEnum.PROJECT
)
node = await self.f4_project_repo.get(project_id=node_id, level=level)
if not node:
raise SheetValidationError(
"Программа не найдена" if node_type == "program" else "Проект не найден"
)
if user.role_id != UserRoleEnum.ADMIN:
user = await self.user_repo.get(user_id=user.id, load_orgs=True)
form = await self.bf_repo.get(
budget_form_id=node.form_id,
org_unit=[ou.id for ou in user.org_units],
)
if not form:
raise SheetValidationError(
"Программа не найдена" if node_type == "program" else "Проект не найден"
)
async def delete_line( async def delete_line(
self, self,
form_id: int, form_id: int,