from alembic import op import sqlalchemy as sa revision = "0009" down_revision = "0008" branch_labels = None depends_on = None def upgrade() -> None: op.execute( """ CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_report_type VARCHAR; v_inserted INTEGER; v_org_unit INTEGER; v_utc_offset VARCHAR(3); BEGIN SELECT report_type, org_unit_id INTO v_report_type, v_org_unit FROM v3.rf_project_report AS rpr INNER JOIN v3.project AS p ON p.id = rpr.project_id WHERE rpr.id = p_report_id; IF NOT FOUND THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; SELECT utc_offset INTO v_utc_offset FROM v3.org_unit WHERE id = v_org_unit; INSERT INTO v3.form3_phase (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at) SELECT p_report_id, pt.phase_code, pt.role, pt.column_keys, (pt.opens_at::text || v_utc_offset)::timestamptz, (pt.closes_at::text || v_utc_offset)::timestamptz FROM v3.phase_template pt WHERE pt.form_type = 'FORM_3' AND pt.sheet = v_report_type ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING; GET DIAGNOSTICS v_inserted = ROW_COUNT; RETURN v_inserted; END; $function$ ; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.add_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone) RETURNS v3.form3_phase LANGUAGE plpgsql AS $function$ DECLARE v_phase v3.form3_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_utc_offset VARCHAR(3); v_report_type VARCHAR; v_project_id INT; BEGIN SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id FROM v3.rf_project_report rpr LEFT JOIN v3.project p ON p.id = rpr.project_id LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id WHERE rpr.id = p_rf_project_report_id; INSERT INTO v3.form3_phase (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at) VALUES ( p_rf_project_report_id, p_phase_code, p_role, p_column_keys, CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END, CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END ) RETURNING * INTO v_phase; v_task_name := trim(concat( 'FORM_3', CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'create', 'phase_id', v_phase_id, 'task_name', NULLIF(v_task_name, ''), 'report_id', p_rf_project_report_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'year', v_year, 'project_id', v_project_id, 'report_type', v_report_type, 'phase_code', p_phase_code, 'role', v_phase.role, 'column_keys', v_phase.column_keys, 'opens_at', v_phase.opens_at, 'closes_at', v_phase.closes_at ), null, null, v_org_unit_id ); RETURN v_phase; END; $function$ ; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.upd_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS v3.form3_phase LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form3_phase; v_new v3.form3_phase; v_is_extend BOOLEAN := FALSE; v_has_non_extend_change BOOLEAN := FALSE; v_has_window_change BOOLEAN := FALSE; v_changes JSONB := '{}'::jsonb; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_utc_offset VARCHAR(3); v_opens_at timestamptz; v_closes_at timestamptz; v_report_type VARCHAR; v_project_id INT; BEGIN SELECT * INTO v_old FROM v3.form3_phase WHERE rf_project_report_id = p_rf_project_report_id AND phase_code = p_phase_code; IF NOT FOUND THEN RAISE EXCEPTION 'form3_phase не найден (rf_project_report_id=%, phase=%)', p_rf_project_report_id, p_phase_code; END IF; SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id FROM v3.rf_project_report rpr LEFT JOIN v3.project p ON p.id = rpr.project_id LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id WHERE rpr.id = p_rf_project_report_id; v_task_name := trim(concat( 'FORM_3', CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); IF p_opens_at is not null THEN v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz; END IF; IF p_closes_at is not null THEN v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz; END IF; UPDATE v3.form3_phase SET role = COALESCE(p_role, role), column_keys = COALESCE(p_column_keys, column_keys), opens_at = COALESCE(v_opens_at, opens_at), closes_at = COALESCE(v_closes_at, closes_at) WHERE rf_project_report_id = p_rf_project_report_id AND phase_code = p_phase_code RETURNING * INTO v_new; v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code); v_is_extend := v_new.closes_at > v_old.closes_at; v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role OR v_old.column_keys IS DISTINCT FROM v_new.column_keys OR v_old.opens_at IS DISTINCT FROM v_new.opens_at; v_has_window_change := v_has_non_extend_change OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend); IF v_old.role IS DISTINCT FROM v_new.role THEN v_changes := v_changes || jsonb_build_object( 'role', jsonb_build_object('before', v_old.role, 'after', v_new.role) ); END IF; IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN v_changes := v_changes || jsonb_build_object( 'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys) ); END IF; IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN v_changes := v_changes || jsonb_build_object( 'opens_at', jsonb_build_object( 'before', v_old.opens_at, 'after', v_new.opens_at ) ); END IF; IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN v_changes := v_changes || jsonb_build_object( 'closes_at', jsonb_build_object( 'before', v_old.closes_at, 'after', v_new.closes_at ) ); END IF; IF v_is_extend THEN PERFORM v3.log_event( 'ACCESS_EXTEND', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'update', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_name', NULLIF(v_task_name, ''), 'report_id', p_rf_project_report_id, 'report_type', v_report_type, 'project_id', v_project_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'year', v_year, 'phase_code', p_phase_code, 'closes_at_before', v_old.closes_at, 'closes_at_after', v_new.closes_at ), null, null, v_org_unit_id ); END IF; IF v_has_window_change THEN PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'update', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_rf_project_report_id, 'task_name', NULLIF(v_task_name, ''), 'project_id', v_project_id, 'report_type', v_report_type, 'year', v_year, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'phase_code', p_phase_code, 'changes', v_changes ), null, null, v_org_unit_id ); END IF; RETURN v_new; END; $function$; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.del_form3_phase(p_rf_project_report_id integer, p_phase_code character varying) RETURNS boolean LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form3_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_report_type VARCHAR; v_project_id INT; BEGIN DELETE FROM v3.form3_phase WHERE rf_project_report_id = p_rf_project_report_id AND phase_code = p_phase_code RETURNING * INTO v_old; IF NOT FOUND THEN RETURN FALSE; END IF; SELECT p.org_unit_id, ou.title, rpr.year, rpr.report_type, rpr.project_id INTO v_org_unit_id, v_org_unit_title, v_year, v_report_type, v_project_id FROM v3.rf_project_report rpr LEFT JOIN v3.project p ON p.id = rpr.project_id LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id WHERE rpr.id = p_rf_project_report_id; v_task_name := trim(concat( 'FORM_3', CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'delete', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_name', NULLIF(v_task_name, ''), 'project_id', v_project_id, 'year', v_year, 'report_type', v_report_type, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'phase_code', p_phase_code, 'role', v_old.role, 'column_keys', v_old.column_keys, 'opens_at', v_old.opens_at, 'closes_at', v_old.closes_at ), null, null, v_org_unit_id ); RETURN TRUE; END; $function$ ; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying) RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer) LANGUAGE plpgsql AS $function$ DECLARE v_pid INT; v_lim INT; v_cur INT; BEGIN IF p_name IS NULL OR length(trim(p_name)) = 0 THEN RAISE EXCEPTION 'p_name обязателен'; END IF; IF p_year IS NULL THEN RAISE EXCEPTION 'p_year обязателен'; END IF; IF p_org_unit_id IS NULL THEN RAISE EXCEPTION 'p_org_unit_id обязателен'; END IF; IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id; END IF; IF p_parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id; END IF; -- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type -- из migrate_form3_v1.sql применятся автоматически). INSERT INTO v3.project ( name, level, parent_id, org_unit_id, project_type, vsp_format, placement_type, object_address, staff_count, total_area, system_code ) VALUES ( p_name, p_level, p_parent_id, p_org_unit_id, p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area, p_system_code ) RETURNING id INTO v_pid; -- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей. -- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие -- годы добавляются отдельно через v3.add_project_year. SELECT y.limit_report_id, y.current_expenses_report_id INTO v_lim, v_cur FROM v3.add_project_year(v_pid, p_year, false) y; -- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов PERFORM v3.log_event( 'PROJECT_CREATE', 'PROJECT', jsonb_strip_nulls(jsonb_build_object( 'project_id', v_pid, 'name', p_name, 'year', p_year, 'level', p_level, 'parent_id', p_parent_id, 'org_unit_id', p_org_unit_id, 'project_type', p_project_type, 'vsp_format', p_vsp_format, 'placement_type', p_placement_type, 'object_address', p_object_address, 'system_code', p_system_code, 'staff_count', p_staff_count, 'total_area', p_total_area, 'limit_report_id', v_lim, 'current_expenses_report_id', v_cur )) ); RETURN QUERY SELECT v_pid, v_lim, v_cur; END; $function$ ; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.upd_phases_timezones(p_org_unit_id integer, p_old_tz character varying, p_new_tz character varying) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_result integer; v_result3 integer; BEGIN IF p_old_tz = p_new_tz THEN RETURN 0; END IF; WITH updated AS ( UPDATE v3.form_phase SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz, closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz WHERE budget_form_id IN (SELECT id FROM v3.budget_form WHERE org_unit_id = p_org_unit_id) AND ( opens_at >= CURRENT_TIMESTAMP OR closes_at >= CURRENT_TIMESTAMP ) RETURNING * ) SELECT COUNT(*) INTO v_result FROM updated; WITH updated AS ( UPDATE v3.form3_phase SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz, closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz WHERE rf_project_report_id IN ( SELECT id FROM V3.rf_project_report rpr WHERE project_id IN ( SELECT id FROM v3.project WHERE org_unit_id = p_org_unit_id ) ) AND ( opens_at >= CURRENT_TIMESTAMP OR closes_at >= CURRENT_TIMESTAMP ) RETURNING * ) SELECT COUNT(*) INTO v_result3 FROM updated; RETURN v_result + v_result3; END; $function$ ; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone) RETURNS v3.form_phase LANGUAGE plpgsql AS $function$ DECLARE v_phase v3.form_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_utc_offset VARCHAR(3); BEGIN SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year FROM v3.budget_form bf LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id WHERE bf.id = p_budget_form_id; SELECT utc_offset INTO v_utc_offset FROM v3.org_unit WHERE id = v_org_unit_id; INSERT INTO v3.form_phase( budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at ) VALUES ( p_budget_form_id, p_sheet, p_phase_code, p_role, p_column_keys, CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END, CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END ) RETURNING * INTO v_phase; v_task_name := trim(concat( COALESCE(v_form_type_code, ''), CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'task', 'entity_id', p_budget_form_id, 'core_entity_type', 'form_phase', 'core_entity_id', v_phase_id, 'action', 'create', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'role', v_phase.role, 'column_keys', v_phase.column_keys, 'opens_at', v_phase.opens_at, 'closes_at', v_phase.closes_at ), p_budget_form_id, null, v_org_unit_id ); RETURN v_phase; END; $function$ ; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.del_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying) RETURNS boolean LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; BEGIN DELETE FROM v3.form_phase WHERE budget_form_id = p_budget_form_id AND sheet = p_sheet AND phase_code = p_phase_code RETURNING * INTO v_old; IF NOT FOUND THEN RETURN FALSE; END IF; SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year FROM v3.budget_form bf LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id WHERE bf.id = p_budget_form_id; v_task_name := trim(concat( COALESCE(v_form_type_code, ''), CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'task', 'entity_id', p_budget_form_id, 'core_entity_type', 'form_phase', 'core_entity_id', v_phase_id, 'action', 'delete', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'task_name', NULLIF(v_task_name, ''), 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'role', v_old.role, 'column_keys', v_old.column_keys, 'opens_at', v_old.opens_at, 'closes_at', v_old.closes_at ), p_budget_form_id, null, v_org_unit_id ); RETURN TRUE; END; $function$ ; """ ) op.execute( """ CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS v3.form_phase LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form_phase; v_new v3.form_phase; v_is_extend BOOLEAN := FALSE; v_has_non_extend_change BOOLEAN := FALSE; v_has_window_change BOOLEAN := FALSE; v_changes JSONB := '{}'::jsonb; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_utc_offset VARCHAR(3); v_opens_at timestamptz; v_closes_at timestamptz; BEGIN SELECT * INTO v_old FROM v3.form_phase WHERE budget_form_id = p_budget_form_id AND sheet = p_sheet AND phase_code = p_phase_code; IF NOT FOUND THEN RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)', p_budget_form_id, p_sheet, p_phase_code; END IF; SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year, ou.utc_offset INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year, v_utc_offset FROM v3.budget_form bf LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id WHERE bf.id = p_budget_form_id; v_task_name := trim(concat( COALESCE(v_form_type_code, ''), CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); IF p_opens_at is not null THEN v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz; END IF; IF p_closes_at is not null THEN v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz; END IF; UPDATE v3.form_phase SET role = COALESCE(p_role, role), column_keys = COALESCE(p_column_keys, column_keys), opens_at = COALESCE(v_opens_at, opens_at), closes_at = COALESCE(v_closes_at, closes_at) WHERE budget_form_id = p_budget_form_id AND sheet = p_sheet AND phase_code = p_phase_code RETURNING * INTO v_new; v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code); v_is_extend := v_new.closes_at > v_old.closes_at; v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role OR v_old.column_keys IS DISTINCT FROM v_new.column_keys OR v_old.opens_at IS DISTINCT FROM v_new.opens_at; v_has_window_change := v_has_non_extend_change OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend); IF v_old.role IS DISTINCT FROM v_new.role THEN v_changes := v_changes || jsonb_build_object( 'role', jsonb_build_object('before', v_old.role, 'after', v_new.role) ); END IF; IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN v_changes := v_changes || jsonb_build_object( 'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys) ); END IF; IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN v_changes := v_changes || jsonb_build_object( 'opens_at', jsonb_build_object( 'before', v_old.opens_at, 'after', v_new.opens_at ) ); END IF; IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN v_changes := v_changes || jsonb_build_object( 'closes_at', jsonb_build_object( 'before', v_old.closes_at, 'after', v_new.closes_at ) ); END IF; IF v_is_extend THEN PERFORM v3.log_event( 'ACCESS_EXTEND', 'ACCESS', jsonb_build_object( 'entity_type', 'task', 'entity_id', p_budget_form_id, 'core_entity_type', 'form_phase', 'core_entity_id', v_phase_id, 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'task_name', NULLIF(v_task_name, ''), 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'closes_at_before', v_old.closes_at, 'closes_at_after', v_new.closes_at ), p_budget_form_id ); END IF; IF v_has_window_change THEN PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'task', 'entity_id', p_budget_form_id, 'core_entity_type', 'form_phase', 'core_entity_id', v_phase_id, 'action', 'update', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'task_name', NULLIF(v_task_name, ''), 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'changes', v_changes ), p_budget_form_id, null, v_org_unit_id ); END IF; RETURN v_new; END; $function$ ; """ ) def downgrade() -> None: pass