insert-new-row: ельзя вставить строку вне своего этапа, оставются ссп и рф при смене роли #142

Merged
tsygankoviva merged 1 commits from insert-row-fix into test 2026-09-02 09:38:07 +03:00
4 changed files with 713 additions and 10 deletions

View File

@ -0,0 +1,15 @@
from sql_migration import run_sql_file_migration
revision = "0026"
down_revision = "0025"
branch_labels = None
depends_on = None
def upgrade() -> None:
run_sql_file_migration("0026_budget_line_stage_access.sql")
def downgrade() -> None:
pass

View File

@ -0,0 +1,681 @@
-- ФОРМА 3
CREATE OR REPLACE FUNCTION v3.add_form3_line(p_report_id integer, p_expense_item_id integer)
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
LANGUAGE plpgsql
AS $function$
DECLARE
v_new_id INT;
v_user_id INT;
v_decision JSONB;
v_msg TEXT;
BEGIN
IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
END IF;
IF NOT EXISTS (SELECT 1 FROM v3.expense_item WHERE id = p_expense_item_id) THEN
RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
END IF;
IF NOT EXISTS (SELECT 1 FROM v3.expense_item_form_type
WHERE expense_item_id = p_expense_item_id AND form_type_code = 'FORM_3') THEN
RAISE EXCEPTION 'expense_item id=% не привязан к form_type FORM_3', p_expense_item_id;
END IF;
v_user_id := v3.current_user_id();
v_decision := v3.can_edit3(p_report_id, 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;
INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
VALUES (p_report_id, p_expense_item_id)
RETURNING id INTO v_new_id;
PERFORM v3.log_event(
'ROW_CREATE', 'ROW_CREATE',
jsonb_build_object(
'sheet', 'FORM_3',
'line_id', v_new_id,
'report_id', p_report_id,
'expense_item_id', p_expense_item_id
)
);
DECLARE v_ei_path INT[];
BEGIN
v_ei_path := v3._expense_item_path_set(ARRAY[p_expense_item_id]);
RETURN QUERY
SELECT v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form3_report_jsonb(p_report_id, NULL) v
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
ORDER BY v.sort_order;
END;
END;
$function$
;
CREATE OR REPLACE FUNCTION v3.can_edit3(p_report_id integer, p_column_key text, p_user_id integer)
RETURNS jsonb
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
v_role VARCHAR;
v_active_close TIMESTAMPTZ;
v_role_has_col BOOLEAN;
v_next_open TIMESTAMPTZ;
v_last_close TIMESTAMPTZ;
BEGIN
IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
END IF;
v_role := v3.user_role_code(p_user_id);
IF v_role IS NULL THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'role_not_allowed',
'detail', 'unknown or inactive user'
);
END IF;
IF v_role = 'ADMIN' THEN
RETURN jsonb_build_object('ok', true, 'admin', true);
END IF;
IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'org_not_assigned',
'detail', p_column_key
);
END IF;
SELECT MAX(fp.closes_at)
INTO v_active_close
FROM v3.form3_phase fp
WHERE fp.rf_project_report_id = p_report_id
AND fp.role = v_role
AND (p_column_key is null or p_column_key = ANY(fp.column_keys))
AND now() BETWEEN fp.opens_at AND fp.closes_at;
IF v_active_close IS NOT NULL THEN
RETURN jsonb_build_object(
'ok', true,
'closes_at', v_active_close
);
END IF;
SELECT EXISTS (
SELECT 1
FROM v3.form3_phase fp
WHERE fp.rf_project_report_id = p_report_id
AND fp.role = v_role
AND p_column_key = ANY(fp.column_keys)
) INTO v_role_has_col;
IF NOT v_role_has_col THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'role_not_allowed',
'detail', p_column_key
);
END IF;
SELECT MIN(fp.opens_at)
INTO v_next_open
FROM v3.form3_phase fp
WHERE fp.rf_project_report_id = p_report_id
AND fp.role = v_role
AND p_column_key = ANY(fp.column_keys)
AND fp.opens_at > now();
IF v_next_open IS NOT NULL THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'window_closed',
'detail', p_column_key,
'opens_at', v_next_open
);
END IF;
SELECT MAX(fp.closes_at)
INTO v_last_close
FROM v3.form3_phase fp
WHERE fp.rf_project_report_id = p_report_id
AND fp.role = v_role
AND p_column_key = ANY(fp.column_keys)
AND fp.closes_at <= now();
RETURN jsonb_build_object(
'ok', false,
'code', 'window_closed',
'detail', p_column_key,
'closes_at', v_last_close
);
END;
$function$
;
-- Конец форма 3
-- Формы 1 2 4
-- DROP FUNCTION v3.add_budget_line(int4, int4, varchar, varchar, varchar, varchar, varchar, varchar, int4, int4, varchar, varchar, date);
CREATE OR REPLACE FUNCTION v3.add_budget_line(p_form_id integer, p_expense_item_id integer DEFAULT NULL::integer, p_sheet character varying DEFAULT NULL::character varying, p_item_id character varying DEFAULT NULL::character varying, p_section_code character varying DEFAULT NULL::character varying, p_direction character varying DEFAULT NULL::character varying, p_name character varying DEFAULT NULL::character varying, p_internal_order character varying DEFAULT NULL::character varying, p_vsp_id integer DEFAULT NULL::integer, p_project_id integer DEFAULT NULL::integer, p_justification character varying DEFAULT NULL::character varying, p_contract_number character varying DEFAULT NULL::character varying, p_contract_end_date date DEFAULT NULL::date)
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
LANGUAGE plpgsql
AS $function$
DECLARE
v_form_type VARCHAR;
v_eid INT;
v_sheet VARCHAR;
v_auto_dir VARCHAR;
v_final_dir VARCHAR;
v_new_id INT;
v_cnt INT;
v_sat_table TEXT;
v_sat_eid INT;
v_parent_line INT;
v_decision JSONB;
v_msg TEXT;
v_user_id INT;
BEGIN
SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id;
IF v_form_type IS NULL THEN
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
END IF;
IF v_form_type = 'FORM_3' THEN
RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)';
END IF;
v_user_id := v3.current_user_id();
v_decision := v3.can_edit(p_form_id, p_sheet, p_direction, 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_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;
v_sat_eid := CASE p_sheet
WHEN 'AHR_RENT' THEN 293 -- R061031001 "Аренда_ОН"
WHEN 'AHR_UTILITY' THEN 24 -- 1.05.1. "Коммунальные услуги"
WHEN 'AHR_SECURITY' THEN 779 -- R061062001 "Пультовая охр.ОН"
END;
IF p_vsp_id IS NULL THEN
RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet;
END IF;
IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
END IF;
SELECT id INTO v_parent_line
FROM v3.budget_line
WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid
LIMIT 1;
IF v_parent_line IS NULL THEN
INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction)
VALUES (
p_form_id, v_sat_eid,
CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END
) RETURNING id INTO v_parent_line;
END IF;
EXECUTE format(
'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) '
|| 'VALUES ($1, $2, $3, $4) RETURNING id',
v_sat_table
) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date
INTO v_new_id;
RETURN QUERY
SELECT v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id;
RETURN;
END IF;
IF p_expense_item_id IS NOT NULL THEN
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
FROM v3.expense_item ei WHERE ei.id = p_expense_item_id;
IF v_eid IS NULL THEN
RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
END IF;
IF NOT EXISTS (
SELECT 1 FROM v3.expense_item_form_type
WHERE expense_item_id = v_eid AND form_type_code = v_form_type
) THEN
RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type;
END IF;
ELSE
IF p_sheet IS NULL OR p_item_id IS NULL THEN
RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)';
END IF;
SELECT count(*) INTO v_cnt
FROM v3.expense_item ei
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
WHERE ei.item_id = p_item_id
AND ei.sheet = p_sheet
AND eift.form_type_code = v_form_type
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
IF v_cnt = 0 THEN
RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%',
p_sheet, p_item_id, p_section_code, p_direction, v_form_type;
END IF;
IF v_cnt > 1 THEN
RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую',
v_cnt, p_sheet, p_item_id, p_section_code, p_direction;
END IF;
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
FROM v3.expense_item ei
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
WHERE ei.item_id = p_item_id
AND ei.sheet = p_sheet
AND eift.form_type_code = v_form_type
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
END IF;
v_auto_dir := CASE
WHEN v_form_type = 'FORM_2' THEN 'Support'
WHEN v_form_type = 'FORM_4' THEN 'Development'
WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL
ELSE NULL
END;
IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN
RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet;
END IF;
IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN
RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)',
p_direction, v_form_type, v_auto_dir;
END IF;
IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN
RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction;
END IF;
v_final_dir := COALESCE(p_direction, v_auto_dir);
IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
END IF;
IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.form4_project WHERE id = p_project_id) THEN
RAISE EXCEPTION 'form4_project id=% не существует', p_project_id;
END IF;
INSERT INTO v3.budget_line (
budget_form_id, expense_item_id, name, internal_order,
vsp_id, project_id, direction, justification
) VALUES (
p_form_id, v_eid, p_name, p_internal_order,
p_vsp_id, p_project_id, v_final_dir, p_justification
) RETURNING id INTO v_new_id;
PERFORM v3.log_event(
'ROW_CREATE', 'ROW',
jsonb_build_object(
'sheet', v_sheet,
'line_id', v_new_id,
'expense_item_id', v_eid,
'direction', v_final_dir,
'name', p_name,
'vsp_id', p_vsp_id,
'project_id', p_project_id
),
p_form_id
);
DECLARE v_ei_path INT[];
BEGIN
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
RETURN QUERY
SELECT v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
ORDER BY v.sort_order;
END;
END;
$function$
;
-- DROP FUNCTION v3.add_form4_program(varchar, int4, varchar);
CREATE OR REPLACE FUNCTION v3.add_form4_program(p_name character varying, p_form_id integer, p_section_code character varying DEFAULT NULL::character varying)
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_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 p_form_id IS NULL THEN
RAISE EXCEPTION 'p_form_id обязателен';
END IF;
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 IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM v3.expense_item
WHERE section_code = p_section_code AND depth = 0
) THEN
RAISE EXCEPTION 'section_code=% не существует в expense_item (depth=0)', p_section_code;
END IF;
v_user_id := v3.current_user_id();
v_decision := v3.can_edit(p_form_id, p_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;
INSERT INTO v3.form4_project (name, level, parent_id, form_id, section_code)
VALUES (p_name, 'program', NULL, p_form_id, p_section_code)
RETURNING id INTO v_pid;
PERFORM v3.log_event(
'PROJECT_CREATE', 'PROJECT',
jsonb_strip_nulls(jsonb_build_object(
'project_id', v_pid,
'name', p_name,
'level', 'program',
'form_id', p_form_id,
'form_type', 'FORM_4'
)),
p_form_id, NULL, NULL
);
RETURN QUERY
SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form4_sheet_jsonb(p_form_id, p_section_code, NULL) v
WHERE (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = v_pid)
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = p_section_code)
ORDER BY v.sort_order;
END;
$function$
;
-- DROP FUNCTION v3.add_form4_project(varchar, int4);
CREATE OR REPLACE FUNCTION v3.add_form4_project(p_name character varying, p_program_id integer)
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 p_program_id IS NULL THEN
RAISE EXCEPTION 'p_program_id обязателен (проект создаётся под программой)';
END IF;
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';
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;
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;
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,
'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.can_edit(p_form_id integer, p_sheet character varying, p_direction character varying, p_column_key text, p_user_id integer)
RETURNS jsonb
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
v_role VARCHAR;
v_active_close TIMESTAMPTZ;
v_role_has_col BOOLEAN;
v_next_open TIMESTAMPTZ;
v_last_close TIMESTAMPTZ;
BEGIN
IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
END IF;
v_role := v3.user_role_code(p_user_id);
IF v_role IS NULL THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'role_not_allowed',
'detail', 'unknown or inactive user'
);
END IF;
IF v_role = 'ADMIN' THEN
RETURN jsonb_build_object('ok', true, 'admin', true);
END IF;
IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_form_org(p_user_id, p_form_id) THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'org_not_assigned',
'detail', p_column_key
);
END IF;
SELECT MAX(fp.closes_at)
INTO v_active_close
FROM v3.form_phase fp
WHERE fp.budget_form_id = p_form_id
AND fp.sheet = p_sheet
AND (fp.direction = p_direction OR (fp.direction IS NULL AND p_direction IS NULL))
AND fp.role = v_role
AND (p_column_key IS NULL OR p_column_key = ANY(fp.column_keys))
AND now() BETWEEN fp.opens_at AND fp.closes_at;
IF v_active_close IS NOT NULL THEN
RETURN jsonb_build_object(
'ok', true,
'closes_at', v_active_close
);
END IF;
SELECT EXISTS (
SELECT 1
FROM v3.form_phase fp
WHERE fp.budget_form_id = p_form_id
AND fp.sheet = p_sheet
AND (fp.direction = p_direction OR (fp.direction IS NULL AND p_direction IS NULL))
AND fp.role = v_role
AND p_column_key = ANY(fp.column_keys)
) INTO v_role_has_col;
IF NOT v_role_has_col THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'role_not_allowed',
'detail', p_column_key
);
END IF;
SELECT MIN(fp.opens_at)
INTO v_next_open
FROM v3.form_phase fp
WHERE fp.budget_form_id = p_form_id
AND fp.sheet = p_sheet
AND (fp.direction = p_direction OR (fp.direction IS NULL AND p_direction IS NULL))
AND fp.role = v_role
AND p_column_key = ANY(fp.column_keys)
AND fp.opens_at > now();
IF v_next_open IS NOT NULL THEN
RETURN jsonb_build_object(
'ok', false,
'code', 'window_closed',
'detail', p_column_key,
'opens_at', v_next_open
);
END IF;
SELECT MAX(fp.closes_at)
INTO v_last_close
FROM v3.form_phase fp
WHERE fp.budget_form_id = p_form_id
AND fp.sheet = p_sheet
AND (fp.direction = p_direction OR (fp.direction IS NULL AND p_direction IS NULL))
AND fp.role = v_role
AND p_column_key = ANY(fp.column_keys)
AND fp.closes_at <= now();
RETURN jsonb_build_object(
'ok', false,
'code', 'window_closed',
'detail', p_column_key,
'closes_at', v_last_close
);
END;
$function$
;

View File

@ -9,8 +9,7 @@ import time
from typing import Any, Optional from typing import Any, Optional
# #
from asyncpg import UniqueViolationError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.exc import IntegrityError
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -26,6 +25,7 @@ from src.services.budget_form_service import BudgetFormService
from src.services.project_service import ProjectService from src.services.project_service import ProjectService
from src.services.sheet_service import SheetService from src.services.sheet_service import SheetService
from src.core.errors import BasicAppException, ValidationsError from src.core.errors import BasicAppException, ValidationsError
from src.core.exception_handlers import map_sqlalchemy_error
from src.db.session import SessionLocal from src.db.session import SessionLocal
from src.services.user_service import UserService from src.services.user_service import UserService
@ -962,8 +962,13 @@ async def process_websocket(
data=data, data=data,
websocket=websocket, websocket=websocket,
) )
except IntegrityError as e: except SQLAlchemyError as e:
data["error"] = str(e) _, code, message, field = map_sqlalchemy_error(e)
data["error"] = {
"code": code,
"field": field,
"message": message,
}
await manager.send_back( await manager.send_back(
data=data, data=data,
websocket=websocket, websocket=websocket,

View File

@ -105,14 +105,16 @@ class UserService:
had_many_ssp_role = previous_role_id in ( had_many_ssp_role = previous_role_id in (
UserRoleEnum.ADMIN.value, UserRoleEnum.ADMIN.value,
UserRoleEnum.EXECUTOR_DFIP.value, UserRoleEnum.EXECUTOR_DFIP.value,
UserRoleEnum.EXECUTOR_DURRS.value,
) )
has_many_ssp_role_now = role.value in ( has_many_ssp_role_now = role.value in (
UserRoleEnum.ADMIN.value, UserRoleEnum.ADMIN.value,
UserRoleEnum.EXECUTOR_DFIP.value, UserRoleEnum.EXECUTOR_DFIP.value,
UserRoleEnum.EXECUTOR_DURRS.value,
) )
is_executor_to_dfip_transition = ( is_executor_to_dfip_transition = (
previous_role_id == UserRoleEnum.EXECUTOR_RF.value previous_role_id == UserRoleEnum.EXECUTOR_RF.value
and role.value == UserRoleEnum.EXECUTOR_DFIP.value and role.value in (UserRoleEnum.EXECUTOR_DFIP.value, UserRoleEnum.EXECUTOR_DURRS.value)
) )
if (had_many_ssp_role and not has_many_ssp_role_now) or is_executor_to_dfip_transition: if (had_many_ssp_role and not has_many_ssp_role_now) or is_executor_to_dfip_transition:
await self.user_repo.clear_many_ssp(user_id) await self.user_repo.clear_many_ssp(user_id)