diff --git a/api/alembic/versions/0036_ahr_satellite_delete_guard.py b/api/alembic/versions/0036_ahr_satellite_delete_guard.py new file mode 100644 index 0000000..cd381be --- /dev/null +++ b/api/alembic/versions/0036_ahr_satellite_delete_guard.py @@ -0,0 +1,15 @@ +from sql_migration import run_sql_file_migration + + +revision = "0036" +down_revision = "0035" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + run_sql_file_migration("0036_ahr_satellite_delete_guard.sql") + + +def downgrade() -> None: + pass diff --git a/api/alembic/versions/sql/0036_ahr_satellite_delete_guard.sql b/api/alembic/versions/sql/0036_ahr_satellite_delete_guard.sql new file mode 100644 index 0000000..8e88918 --- /dev/null +++ b/api/alembic/versions/sql/0036_ahr_satellite_delete_guard.sql @@ -0,0 +1,128 @@ +CREATE OR REPLACE FUNCTION v3.del_budget_line(p_id integer, p_direction character varying DEFAULT NULL::character varying, p_sheet character varying DEFAULT NULL::character varying) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql +AS $function$ +DECLARE + v_form_id INT; + v_org_id INT; + v_sheet VARCHAR; + v_sat_table TEXT; + v_eid INT; + v_line_id INT; + v_ei_path INT[]; +BEGIN + IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN + v_sat_table := CASE p_sheet + WHEN 'AHR_RENT' THEN 'rent_detail' + WHEN 'AHR_UTILITY' THEN 'utility_detail' + WHEN 'AHR_SECURITY' THEN 'security_detail' + END; + + EXECUTE format( + 'SELECT bl.budget_form_id FROM v3.%I s + JOIN v3.budget_line bl ON bl.id = s.line_id + WHERE s.id = $1', v_sat_table + ) USING p_id INTO v_form_id; + + IF v_form_id IS NULL THEN + RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_id; + END IF; + + SELECT m.target_line_id + INTO v_line_id + FROM v3.form2_ahr_satellite_line_source m + WHERE m.source_sheet = p_sheet + AND m.source_id = p_id; + + EXECUTE format('DELETE FROM v3.%I WHERE id = $1', v_sat_table) USING p_id; + + IF v_line_id IS NOT NULL THEN + SELECT bl.expense_item_id + INTO v_eid + FROM v3.budget_line bl + WHERE bl.id = v_line_id; + + DELETE FROM v3.plan WHERE line_id = v_line_id; + DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id; + DELETE FROM v3.budget_line WHERE id = v_line_id; + + SELECT org_unit_id INTO v_org_id FROM v3.budget_form WHERE id = v_form_id; + + PERFORM v3.log_event( + 'ROW_DELETE', 'ROW', + jsonb_build_object( + 'sheet', 'AHR', + 'line_id', v_line_id, + 'entity_id', v_line_id, + 'expense_item_id', v_eid, + 'direction', 'Support', + 'source_sheet', p_sheet, + 'source_id', p_id + ), + v_form_id, NULL, v_org_id + ); + END IF; + RETURN; + END IF; + + SELECT bl.budget_form_id, ei.sheet, bl.expense_item_id + INTO v_form_id, v_sheet, v_eid + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + WHERE bl.id = p_id; + + IF v_form_id IS NULL THEN + RAISE EXCEPTION 'budget_line id=% не существует', p_id; + END IF; + + IF v_sheet = 'AHR' AND EXISTS ( + SELECT 1 + FROM v3.form2_ahr_satellite_line_source m + WHERE m.target_line_id = p_id + ) THEN + RAISE EXCEPTION 'Строку АХР, созданную из листа-сателлита, нельзя удалить напрямую' + USING ERRCODE = '23503', + HINT = 'Удалите строку на исходном листе АХР Аренда / АХР Коммунальные услуги / АХР Безопасность'; + END IF; + + v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]); + + DELETE FROM v3.plan WHERE line_id = p_id; + DELETE FROM v3.sequestration WHERE line_id = p_id; + DELETE FROM v3.reserve WHERE line_id = p_id; + DELETE FROM v3.allocation WHERE line_id = p_id; + DELETE FROM v3.contract_summary WHERE line_id = p_id; + DELETE FROM v3.collegial_approval WHERE line_id = p_id; + DELETE FROM v3.ckk WHERE line_id = p_id; + DELETE FROM v3.contract_detail WHERE line_id = p_id; + DELETE FROM v3.booking WHERE line_id = p_id; + DELETE FROM v3.budget_line_quarter WHERE line_id = p_id; + DELETE FROM v3.rent_detail WHERE line_id = p_id; + DELETE FROM v3.utility_detail WHERE line_id = p_id; + DELETE FROM v3.security_detail WHERE line_id = p_id; + + DELETE FROM v3.budget_line WHERE id = p_id; + + SELECT org_unit_id INTO v_org_id FROM v3.budget_form WHERE id = v_form_id; + + PERFORM v3.log_event( + 'ROW_DELETE', 'ROW', + jsonb_build_object( + 'sheet', v_sheet, + 'line_id', p_id, + 'entity_id', p_id, + 'expense_item_id', v_eid, + 'direction', p_direction + ), + v_form_id, NULL, v_org_id + ); + + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data + FROM v3.v_form_view(v_form_id, v_sheet, NULL, p_direction) v + WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM') + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path) + ORDER BY v.sort_order; +END; +$function$; + diff --git a/api/src/api/v1/websocket.py b/api/src/api/v1/websocket.py index a892153..4389254 100644 --- a/api/src/api/v1/websocket.py +++ b/api/src/api/v1/websocket.py @@ -9,6 +9,7 @@ import time from typing import Any, Optional # +from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status from sqlalchemy.ext.asyncio import AsyncSession @@ -800,6 +801,49 @@ async def notify_ahr_about_satellite_change( user_id: int, ): try: + if source_event == "row_deleted": + async with get_db_session(user_id=user_id) as db: + deleted_line_id = ( + await db.execute( + text( + """ + SELECT (event_data->>'line_id')::int + FROM v3.audit_log + WHERE form_id = :form_id + AND event = 'ROW_DELETE' + AND event_type = 'ROW' + AND event_data->>'sheet' = 'AHR' + AND event_data->>'source_sheet' = :source_sheet + AND (event_data->>'source_id')::int = :source_row_id + ORDER BY id DESC + LIMIT 1 + """ + ), + { + "form_id": form_id, + "source_sheet": source_sheet, + "source_row_id": source_row_id, + }, + ) + ).scalar_one_or_none() + + if deleted_line_id is None: + return + + await manager.broadcast_to_form_sheet( + data={ + "event": "row_deleted", + "data": { + "row_id": deleted_line_id, + "source_sheet": source_sheet, + }, + "result": [], + }, + form_id=form_id, + sheet="AHR", + ) + return + async with get_db_session(user_id=user_id) as db: ahr_delta = await SheetService(db).get_form2_ahr_satellite_delta( form_id=form_id,