Merge pull request 'insert-new-row: ельзя вставить строку вне своего этапа, оставются ссп и рф при смене роли' (#142) from insert-row-fix into test

Reviewed-on: #142
This commit is contained in:
tsygankoviva 2026-09-02 09:38:06 +03:00
commit e8b705a0e9
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 asyncpg import UniqueViolationError
from sqlalchemy.exc import IntegrityError
from sqlalchemy.exc import SQLAlchemyError
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
from sqlalchemy.ext.asyncio import AsyncSession
@ -25,7 +24,8 @@ from src.db.models.form_type import FormTypeEnum
from src.services.budget_form_service import BudgetFormService
from src.services.project_service import ProjectService
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.services.user_service import UserService
@ -962,12 +962,17 @@ async def process_websocket(
data=data,
websocket=websocket,
)
except IntegrityError as e:
data["error"] = str(e)
await manager.send_back(
data=data,
websocket=websocket,
)
except SQLAlchemyError as e:
_, code, message, field = map_sqlalchemy_error(e)
data["error"] = {
"code": code,
"field": field,
"message": message,
}
await manager.send_back(
data=data,
websocket=websocket,
)
except SheetValidationError as e:
data["error"] = e.message
await manager.send_back(

View File

@ -105,14 +105,16 @@ class UserService:
had_many_ssp_role = previous_role_id in (
UserRoleEnum.ADMIN.value,
UserRoleEnum.EXECUTOR_DFIP.value,
UserRoleEnum.EXECUTOR_DURRS.value,
)
has_many_ssp_role_now = role.value in (
UserRoleEnum.ADMIN.value,
UserRoleEnum.EXECUTOR_DFIP.value,
UserRoleEnum.EXECUTOR_DURRS.value,
)
is_executor_to_dfip_transition = (
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:
await self.user_repo.clear_many_ssp(user_id)