-- ════════════════════════════════════════════════════════════════════════════ -- МИГРАЦИЯ: Добавление поля direction в phase_template и form_phase -- ════════════════════════════════════════════════════════════════════════════ -- Добавляем direction ALTER TABLE v3.phase_template ADD COLUMN direction VARCHAR; ALTER TABLE v3.form_phase ADD COLUMN direction VARCHAR; ALTER TABLE v3.form_phase DROP CONSTRAINT IF EXISTS form_phase_pkey; CREATE UNIQUE INDEX form_phase_uidx ON v3.form_phase(budget_form_id, sheet, direction, phase_code); ALTER TABLE v3.phase_template DROP CONSTRAINT IF EXISTS phase_template_pkey; CREATE UNIQUE INDEX phase_template_uidx ON v3.phase_template(form_type, sheet, direction, phase_code); -- Добаввляем direction во все нужные функции фаз и шаблонов фаз DROP FUNCTION IF EXISTS v3.add_form_phase(int4, varchar, varchar, varchar, _text, timestamp without time zone, timestamp without time zone); CREATE OR REPLACE FUNCTION v3.add_form_phase( p_budget_form_id integer, p_sheet character varying, p_direction 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, direction, phase_code, role, column_keys, opens_at, closes_at ) VALUES ( p_budget_form_id, p_sheet, p_direction, 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, 'direction', p_direction, '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$ ; DROP FUNCTION IF EXISTS v3.can_edit(int4, varchar, text, int4); 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; -- ADMIN — bypass всех проверок. IF v_role = 'ADMIN' THEN RETURN jsonb_build_object('ok', true, 'admin', true); END IF; -- EXECUTOR_RF: фильтр по user_org формы. 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; -- 1. Активная сейчас фаза, в которой эта колонка перечислена для этой роли? 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 = 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; -- 2. Колонка В ПРИНЦИПЕ есть хоть в одной фазе этой роли (на этой форме/листе)? -- Если нет — это вопрос whitelist, отдаём role_not_allowed. 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; -- 3. Колонка доступна, но окно не активно. Подсказать ближайшее. 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$ ; CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_user_id integer, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_change JSONB; v_column TEXT; v_decision JSONB; v_msg TEXT; BEGIN IF p_user_id IS NULL THEN RAISE EXCEPTION 'role_not_allowed: '; END IF; IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id; END IF; IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN RAISE EXCEPTION 'p_changes must be a JSONB array'; END IF; -- Проверяем КАЖДУЮ ячейку до начала записи. Падаем на первой запрещённой — -- транзакция целиком откатится, частичной записи не будет. FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes) LOOP v_column := v_change->>'column'; IF v_column IS NULL THEN RAISE EXCEPTION 'change must have column: %', v_change; END IF; v_decision := v3.can_edit(p_form_id, p_sheet, p_direction, v_column, p_user_id); IF NOT (v_decision->>'ok')::boolean THEN IF v_decision->>'code' = 'role_not_allowed' THEN RAISE EXCEPTION 'role_not_allowed: %', v_column; ELSIF v_decision->>'code' = 'org_not_assigned' THEN RAISE EXCEPTION 'org_not_assigned: %', v_column; ELSIF v_decision->>'code' = 'window_closed' THEN IF v_decision ? 'opens_at' THEN v_msg := format('window_closed: %s (opens %s)', v_column, v_decision->>'opens_at'); ELSIF v_decision->>'closes_at' IS NOT NULL THEN v_msg := format('window_closed: %s (closed %s)', v_column, v_decision->>'closes_at'); ELSE v_msg := format('window_closed: %s (no phase)', v_column); END IF; RAISE EXCEPTION '%', v_msg; ELSE RAISE EXCEPTION '%: %', v_decision->>'code', v_column; END IF; END IF; END LOOP; -- Все ячейки разрешены — делегируем в v3. -- ROW_UPDATE намеренно НЕ логируется (см. logging_events.md): cell-edit'ы -- слишком частые. В audit_log пишем только структурные события -- (ROW_CREATE / ROW_DELETE) + бизнес-операции (CREATE_TASK и т.д.). RETURN QUERY SELECT * FROM v3.upd_form_cells( p_form_id, p_sheet, p_changes, p_direction, p_sections ); END; $function$ ; CREATE OR REPLACE FUNCTION v3.copy_template_to_form(p_form_id integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_form_type VARCHAR; v_inserted INTEGER; v_utc_offset VARCHAR(3); v_org_unit INTEGER; v_year INTEGER; BEGIN SELECT form_type_code, org_unit_id, "year" INTO v_form_type, v_org_unit, v_year FROM v3.budget_form WHERE id = p_form_id; IF NOT FOUND THEN RAISE EXCEPTION 'budget_form id=% не существует', p_form_id; END IF; SELECT utc_offset INTO v_utc_offset FROM v3.org_unit WHERE id = v_org_unit; INSERT INTO v3.form_phase (budget_form_id, sheet, direction, phase_code, role, column_keys, opens_at, closes_at) SELECT p_form_id, pt.sheet, pt.direction, pt.phase_code, pt.role, pt.column_keys, ((pt.opens_at + INTERVAL '1 year' * v_year)::text || v_utc_offset)::timestamptz, ((pt.closes_at + INTERVAL '1 year' * v_year)::text || v_utc_offset)::timestamptz FROM v3.phase_template pt WHERE pt.form_type = v_form_type ON CONFLICT (budget_form_id, sheet, phase_code, direction) DO NOTHING; GET DIAGNOSTICS v_inserted = ROW_COUNT; RETURN v_inserted; END; $function$ ; -- DROP FUNCTION v3.copy_template_to_report(int4); 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); v_year INTEGER; BEGIN SELECT report_type, org_unit_id, "year" INTO v_report_type, v_org_unit, v_year 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 + INTERVAL '1 year' * v_year)::text || v_utc_offset)::timestamptz, ((pt.closes_at + INTERVAL '1 year' * v_year)::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$ ; DROP FUNCTION IF EXISTS v3.del_form_phase(int4, varchar, varchar); CREATE OR REPLACE FUNCTION v3.del_form_phase(p_budget_form_id integer, p_sheet character varying, p_direction 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 (direction = p_direction OR (direction IS NULL AND p_direction IS NULL)) 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, 'direction', p_direction, '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$ ; DROP FUNCTION IF EXISTS v3.editable_columns_for(int4, int4, varchar); CREATE OR REPLACE FUNCTION v3.editable_columns_for(p_form_id integer, p_user_id integer, p_sheet character varying, p_direction character varying) RETURNS TABLE(column_key text, closes_at timestamp with time zone) LANGUAGE plpgsql STABLE AS $function$ DECLARE v_role VARCHAR; 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; -- неизвестный/неактивный юзер → пустая маска END IF; -- ADMIN: вернуть UNION всех column_keys всех phase'ов на форме/листе. IF v_role = 'ADMIN' THEN RETURN QUERY SELECT ck AS column_key, MAX(fp.closes_at) AS closes_at FROM v3.form_phase fp, LATERAL unnest(fp.column_keys) AS ck 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)) GROUP BY ck; RETURN; END IF; -- EXECUTOR_RF без назначения → пустая маска (default deny). IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_form_org(p_user_id, p_form_id) THEN RETURN; END IF; RETURN QUERY SELECT ck AS column_key, MAX(fp.closes_at) AS closes_at FROM v3.form_phase fp, LATERAL unnest(fp.column_keys) AS ck 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 now() BETWEEN fp.opens_at AND fp.closes_at GROUP BY ck; END; $function$ ; CREATE OR REPLACE FUNCTION v3.v_form_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_direction character varying DEFAULT NULL::character varying, p_user_id integer DEFAULT NULL::integer) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql STABLE AS $function$ DECLARE v_form_type VARCHAR; v_year INT; v_org_unit_id INT; BEGIN SELECT form_type_code, year, org_unit_id INTO v_form_type, v_year, v_org_unit_id FROM v3.budget_form WHERE id = p_form_id; IF NOT FOUND THEN RAISE EXCEPTION 'budget_form id=% не существует в v3.budget_form', p_form_id; END IF; IF p_user_id IS NOT NULL THEN RETURN QUERY SELECT 'META'::VARCHAR, -1, 0::BIGINT, jsonb_build_object( 'user_id', p_user_id, 'role', v3.user_role_code(p_user_id), 'sheet', p_sheet, 'editable', COALESCE( (SELECT jsonb_agg( jsonb_build_object('column', ec.column_key, 'closes_at', ec.closes_at) ORDER BY ec.column_key) FROM v3.editable_columns_for(p_form_id, p_user_id, p_sheet, p_direction) ec), '[]'::JSONB) ); END IF; IF v_form_type = 'FORM_1' THEN IF p_sheet = 'SMETA' THEN RETURN QUERY SELECT sm.row_type, sm.depth, ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, jsonb_build_object( 'section_code', sm.section_code, 'name', sm.name, 'plan', jsonb_build_object( 'support', jsonb_build_object('q1', sm.supp_plan_q1, 'q2', sm.supp_plan_q2, 'q3', sm.supp_plan_q3, 'q4', sm.supp_plan_q4, 'year', sm.supp_plan_year), 'development', jsonb_build_object('q1', sm.dev_plan_q1, 'q2', sm.dev_plan_q2, 'q3', sm.dev_plan_q3, 'q4', sm.dev_plan_q4, 'year', sm.dev_plan_year), 'total_year', sm.total_plan_year), 'approved', jsonb_build_object( 'support', jsonb_build_object('q1', sm.supp_appr_q1, 'q2', sm.supp_appr_q2, 'q3', sm.supp_appr_q3, 'q4', sm.supp_appr_q4, 'year', sm.supp_appr_year), 'development', jsonb_build_object('q1', sm.dev_appr_q1, 'q2', sm.dev_appr_q2, 'q3', sm.dev_appr_q3, 'q4', sm.dev_appr_q4, 'year', sm.dev_appr_year), 'total_year', sm.total_appr_year), 'fact', jsonb_build_object( 'support', jsonb_build_object('q1', sm.supp_act_q1, 'q2', sm.supp_act_q2, 'q3', sm.supp_act_q3, 'q4', sm.supp_act_q4, 'year', sm.supp_act_year), 'development', jsonb_build_object('q1', sm.dev_act_q1, 'q2', sm.dev_act_q2, 'q3', sm.dev_act_q3, 'q4', sm.dev_act_q4, 'year', sm.dev_act_year), 'total_year', sm.total_act_year), 'corrected', jsonb_build_object( 'support', jsonb_build_object('q2', sm.supp_corr_q2, 'q3', sm.supp_corr_q3, 'q4', sm.supp_corr_q4), 'development', jsonb_build_object('q2', sm.dev_corr_q2, 'q3', sm.dev_corr_q3, 'q4', sm.dev_corr_q4)) ) FROM v3.v_form1_smeta(v_year, v_org_unit_id) sm ORDER BY sm.section_code; RETURN; END IF; IF p_sheet IN ('AHR','CAP','OPER') THEN RETURN QUERY SELECT j.row_type, j.depth, ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order, jsonb_build_object( 'line_id', j.line_id, 'header', j.header, 'plan', j.plan_data, 'contract_summary', j.contract_summary, 'allocation', j.allocation_data, 'sequestration', j.sequestration_data, 'reserve', j.reserve_data, 'approved', j.approved_data, 'collegial', j.collegial_data, 'ckk', j.ckk_data, 'contract_detail', j.contract_detail, 'q1', j.q1_data, 'q2', j.q2_data, 'q3', j.q3_data, 'q4', j.q4_data, 'totals', j.totals_data ) FROM v3.v_form1_sheet_jsonb(p_form_id, p_sheet, p_direction, p_sections) j ORDER BY j._sort_path; RETURN; END IF; RAISE EXCEPTION 'Unknown FORM_1 sheet: %', p_sheet USING HINT = 'Use AHR / CAP / OPER / SMETA'; END IF; IF v_form_type = 'FORM_2' THEN RETURN QUERY SELECT * FROM v3.v_form2_view(p_form_id, p_sheet, p_sections); RETURN; END IF; IF v_form_type = 'FORM_4' THEN RETURN QUERY SELECT * FROM v3.v_form4_view(p_form_id, p_sheet, p_sections); RETURN; END IF; RAISE EXCEPTION 'Unsupported form_type=% for form_id=%', v_form_type, p_form_id; END; $function$ ; DROP FUNCTION IF EXISTS v3.upd_form_phase(int4, varchar, varchar, varchar, _text, timestamp, timestamp); CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_direction 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 (direction = p_direction OR (direction IS NULL AND p_direction IS NULL)) 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 (direction = p_direction OR (direction IS NULL AND p_direction IS NULL)) 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$ ; -- Обновляем direction для фаз первой формы UPDATE v3.phase_template pt SET direction = 'Support' WHERE sheet IN ('AHR', 'CAP') AND form_type = 'FORM_1'; INSERT INTO v3.phase_template (form_type, sheet, direction, phase_code, role, column_keys, opens_at, closes_at) SELECT pt.form_type, pt.sheet, 'Development', pt.phase_code, pt.role, pt.column_keys, pt.opens_at, pt.closes_at FROM v3.phase_template pt WHERE pt.sheet IN ('AHR', 'CAP') AND pt.form_type = 'FORM_1'; UPDATE v3.form_phase fp SET direction = 'Support' WHERE sheet IN ('AHR', 'CAP') AND budget_form_id IN ( SELECT id FROM v3.budget_form WHERE form_type_code = 'FORM_1' ); INSERT INTO v3.form_phase (budget_form_id, sheet, direction, phase_code, role, column_keys, opens_at, closes_at) SELECT fp.budget_form_id, fp.sheet, 'Development', fp.phase_code, fp.role, fp.column_keys, fp.opens_at, fp.closes_at FROM v3.form_phase fp WHERE fp.sheet IN ('AHR', 'CAP') AND budget_form_id IN ( SELECT id FROM v3.budget_form WHERE form_type_code = 'FORM_1' ); -- Добавляем возможность менять justification у budget_line CREATE OR REPLACE FUNCTION v3._apply_form_cell(p_line_id integer, p_column text, p_value jsonb, p_sheet character varying DEFAULT NULL::character varying, p_form_id integer DEFAULT NULL::integer) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE v_parts TEXT[]; v_scope TEXT; v_field TEXT; v_q SMALLINT; v_actor TEXT; v_target_table TEXT; v_target_col TEXT; v_target_type TEXT; v_key_kind TEXT; v_str TEXT; v_sql TEXT; v_sat_table TEXT; v_cnt INT; BEGIN v_parts := string_to_array(p_column, '.'); IF p_sheet = 'OTCH9F' THEN DECLARE v_far_month SMALLINT; BEGIN SELECT month INTO v_far_month FROM v3.fixed_asset_report WHERE id = p_line_id; IF v_far_month IS NULL THEN RAISE EXCEPTION 'fixed_asset_report.id=% не существует', p_line_id; END IF; IF array_length(v_parts,1) = 1 THEN IF v_parts[1] = 'equipment_name' THEN RAISE EXCEPTION 'normative_field: equipment_name (нормативная номенклатура, правится отдельным API)'; ELSIF v_parts[1] IN ('month','expense_item_id','item_id','section_code','id') THEN RAISE EXCEPTION 'key_field: % (часть ключа строки, RO)', v_parts[1]; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END IF; END IF; IF array_length(v_parts,1) <> 2 THEN RAISE EXCEPTION 'bad_column_format: %', p_column; END IF; IF v_parts[1] = 'totals' THEN IF v_parts[2] = 'go_balance_only' THEN v_target_col := 'go_balance_only_amt'; v_target_type := 'NUMERIC'; ELSIF v_parts[2] IN ('acquired_total','disposed_total','balance_qty','balance_amt') THEN RAISE EXCEPTION 'computed_field: totals.%', v_parts[2]; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END IF; ELSIF v_parts[1] IN ('b_604','b_60415') THEN IF v_parts[2] LIKE 'opening_%' AND v_far_month <> 1 THEN RAISE EXCEPTION 'computed_field: % (opening для month>1 вычисляется из base + накопит. дельты)', p_column; END IF; IF v_parts[1] = 'b_604' THEN v_target_col := CASE v_parts[2] WHEN 'opening_qty' THEN 'opening_qty_604' WHEN 'opening_amt' THEN 'opening_amt_604' WHEN 'acquired_qty' THEN 'acquired_qty_604' WHEN 'acquired_amt' THEN 'acquired_amt_604' WHEN 'disposed_qty' THEN 'disposed_qty_604' WHEN 'disposed_amt' THEN 'disposed_amt_604' END; IF v_target_col IS NULL THEN RAISE EXCEPTION 'unknown_column: %', p_column; END IF; v_target_type := CASE WHEN v_parts[2] LIKE '%_qty' THEN 'INTEGER' ELSE 'NUMERIC' END; ELSE -- b_60415 v_target_col := CASE v_parts[2] WHEN 'opening_qty' THEN 'opening_qty_60415' WHEN 'opening_amt' THEN 'opening_amt_60415' WHEN 'acquired_qty' THEN 'acquired_qty_60415' WHEN 'acquired_amt' THEN 'acquired_amt_60415' WHEN 'transferred_qty' THEN 'transferred_qty_60415' WHEN 'transferred_amt' THEN 'transferred_amt_60415' END; IF v_target_col IS NULL THEN RAISE EXCEPTION 'unknown_column: %', p_column; END IF; v_target_type := CASE WHEN v_parts[2] LIKE '%_qty' THEN 'INTEGER' ELSE 'NUMERIC' END; END IF; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END IF; EXECUTE format( 'UPDATE v3.fixed_asset_report SET %I = ($2 #>> ''{}'')::%s WHERE id = $1', v_target_col, v_target_type ) USING p_line_id, p_value; RETURN; END; END IF; IF p_sheet = 'AHR_LIMIT' THEN IF p_form_id IS NULL THEN RAISE EXCEPTION 'internal: p_form_id не передан для AHR_LIMIT'; END IF; IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt WHERE lt.id = p_line_id AND lt.row_type = 'LEAF') THEN RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF (SECTION/GROUP не редактируется)', p_line_id; END IF; IF array_length(v_parts,1) <> 1 THEN RAISE EXCEPTION 'bad_column_format: %, expected single key', p_column; END IF; CASE v_parts[1] WHEN 'qty_q1','qty_q2','qty_q3','qty_q4' THEN v_target_col := v_parts[1]; v_target_type := 'INTEGER'; WHEN 'comment' THEN v_target_col := 'comment'; v_target_type := 'TEXT'; WHEN 'amount_q1','amount_q2','amount_q3','amount_q4' THEN RAISE EXCEPTION 'computed_field: % (вычисляется из qty × limit × period_factor)', p_column; WHEN 'name','unit','section_no','expense_item_code', 'limit_with_vat','limit_without_vat' THEN RAISE EXCEPTION 'normative_field: % (нормативный справочник, правится админом отдельно)', p_column; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END CASE; EXECUTE format( 'INSERT INTO v3.form_limit (budget_form_id, template_id, %1$I) ' || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) ' || 'ON CONFLICT (budget_form_id, template_id) DO UPDATE SET %1$I = EXCLUDED.%1$I', v_target_col, v_target_type ) USING p_form_id, p_line_id, p_value; RETURN; 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; IF array_length(v_parts,1) = 1 THEN CASE v_parts[1] WHEN 'contract_number' THEN v_target_col := 'contract_number'; v_target_type := 'TEXT'; WHEN 'contract_end_date' THEN v_target_col := 'contract_end_date'; v_target_type := 'DATE'; WHEN 'comment' THEN v_target_col := 'comment'; v_target_type := 'TEXT'; WHEN 'address','object_type','rented_area','object_area' THEN RAISE EXCEPTION 'computed_field: % (атрибут v3.vsp, правится отдельно)', p_column; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END CASE; ELSIF array_length(v_parts,1) = 2 AND v_parts[1] = 'plan' THEN IF v_parts[2] = 'year' THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; IF v_parts[2] NOT IN ('q1','q2','q3','q4') THEN RAISE EXCEPTION 'unknown_column: %', p_column; END IF; v_target_col := 'plan_' || v_parts[2]; v_target_type := 'NUMERIC'; ELSIF array_length(v_parts,1) = 2 AND v_parts[1] LIKE 'fact_q%' THEN IF v_parts[2] = 'total' THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; IF (v_parts[1]='fact_q1' AND v_parts[2] NOT IN ('jan','feb','mar')) OR (v_parts[1]='fact_q2' AND v_parts[2] NOT IN ('apr','may','jun')) OR (v_parts[1]='fact_q3' AND v_parts[2] NOT IN ('jul','aug','sep')) OR (v_parts[1]='fact_q4' AND v_parts[2] NOT IN ('oct','nov','dec')) THEN RAISE EXCEPTION 'unknown_column: % (месяц вне квартала)', p_column; END IF; v_target_col := 'actual_' || v_parts[2]; v_target_type := 'NUMERIC'; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END IF; EXECUTE format( 'UPDATE v3.%I SET %I = ($2 #>> ''{}'')::%s WHERE id = $1', v_sat_table, v_target_col, v_target_type ) USING p_line_id, p_value; GET DIAGNOSTICS v_cnt = ROW_COUNT; IF v_cnt = 0 THEN RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_line_id; END IF; RETURN; END IF; IF array_length(v_parts, 1) < 2 THEN RAISE EXCEPTION 'bad_column_format: %, expected scope.field', p_column; END IF; v_scope := v_parts[1]; v_field := v_parts[2]; IF v_scope = 'header' THEN IF v_field = 'name' THEN UPDATE v3.budget_line SET name = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id; ELSIF v_field = 'internal_order' THEN UPDATE v3.budget_line SET internal_order = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id; ELSIF v_field = 'justification' THEN UPDATE v3.budget_line SET justification = (p_value #>> '{}')::TEXT, updated_at = now() WHERE id = p_line_id; ELSIF v_field = 'vsp_id' THEN UPDATE v3.budget_line SET vsp_id = (p_value #>> '{}')::INT, updated_at = now() WHERE id = p_line_id; ELSIF v_field IN ('year','section','item_id','num_group','vsp_address') THEN RAISE EXCEPTION 'computed_field: %', p_column; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END IF; RETURN; END IF; IF v_scope IN ('approved','totals') THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; IF v_scope = 'booking' THEN IF array_length(v_parts, 1) <> 3 THEN RAISE EXCEPTION 'bad_column_format: %, expected booking.y{2026|2027}.qN', p_column; END IF; IF v_parts[2] NOT IN ('y2026','y2027') THEN RAISE EXCEPTION 'bad_booking_year: % (only y2026/y2027)', v_parts[2]; END IF; IF v_parts[3] NOT IN ('q1','q2','q3','q4') THEN RAISE EXCEPTION 'bad_booking_quarter: %', v_parts[3]; END IF; v_target_col := CASE v_parts[2] WHEN 'y2026' THEN 'expenses_' || v_parts[3] WHEN 'y2027' THEN 'expenses_next_year_' || v_parts[3] END; v_sql := format( 'INSERT INTO v3.ckk (line_id, %1$I) VALUES ($1, ($2 #>> ''{}'')::NUMERIC) ' || 'ON CONFLICT (line_id) DO UPDATE SET %1$I = EXCLUDED.%1$I', v_target_col ); EXECUTE v_sql USING p_line_id, p_value; UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id; RETURN; END IF; IF v_scope IN ('q1','q2','q3','q4') THEN v_q := substring(v_scope FROM 2)::SMALLINT; ELSE v_q := NULL; END IF; v_actor := CASE v_scope WHEN 'sequestration' THEN 'DFIP' WHEN 'seq_dfip' THEN 'DFIP' WHEN 'seq_ssp' THEN 'SSP_GO' ELSE NULL END; SELECT m.target_table, m.target_col, m.target_type, m.key_kind INTO v_target_table, v_target_col, v_target_type, v_key_kind FROM (VALUES ('plan','q1', 'plan','plan_q1','NUMERIC','line'), ('plan','q2', 'plan','plan_q2','NUMERIC','line'), ('plan','q3', 'plan','plan_q3','NUMERIC','line'), ('plan','q4', 'plan','plan_q4','NUMERIC','line'), ('plan','comment', 'plan','comment','TEXT', 'line'), ('reserve','q1', 'reserve','amount_q1','NUMERIC','line'), ('reserve','q2', 'reserve','amount_q2','NUMERIC','line'), ('reserve','q3', 'reserve','amount_q3','NUMERIC','line'), ('reserve','q4', 'reserve','amount_q4','NUMERIC','line'), ('reserve','justification','reserve','justification','TEXT','line'), ('allocation','order', 'allocation','internal_order','TEXT','line'), ('allocation','property', 'allocation','property_object','TEXT','line'), ('allocation','contract_ref', 'allocation','contract_ref','TEXT','line'), ('allocation','allocation_purpose', 'allocation','allocation_purpose','TEXT','line'), ('contract_summary','total', 'contract_summary','total_amount', 'NUMERIC','line'), ('contract_summary','counterparty', 'contract_summary','counterparty', 'TEXT', 'line'), ('contract_summary','deadline', 'contract_summary','deadline', 'TEXT', 'line'), ('contract_summary','comment', 'contract_summary','comment', 'TEXT', 'line'), ('contract_summary','future_y1', 'contract_summary','future_payments_y1', 'NUMERIC','line'), ('contract_summary','future_y2', 'contract_summary','future_payments_y2', 'NUMERIC','line'), ('contract_summary','other_ssp', 'contract_summary','other_ssp_amount', 'NUMERIC','line'), ('contract_summary','reference', 'contract_summary','reference', 'TEXT','line'), ('contract_summary','centralized_flag', 'contract_summary','centralized_flag', 'TEXT','line'), ('collegial','approved', 'collegial_approval','approved_amount', 'NUMERIC','line'), ('collegial','protocol', 'collegial_approval','protocol_reference','TEXT', 'line'), ('collegial','note', 'collegial_approval','note', 'TEXT', 'line'), ('ckk','ceiling', 'ckk','ceiling_amount', 'NUMERIC','line'), ('ckk','q1', 'ckk','expenses_q1', 'NUMERIC','line'), ('ckk','q2', 'ckk','expenses_q2', 'NUMERIC','line'), ('ckk','q3', 'ckk','expenses_q3', 'NUMERIC','line'), ('ckk','q4', 'ckk','expenses_q4', 'NUMERIC','line'), ('ckk','next_q1', 'ckk','expenses_next_year_q1', 'NUMERIC','line'), ('ckk','next_q2', 'ckk','expenses_next_year_q2', 'NUMERIC','line'), ('ckk','next_q3', 'ckk','expenses_next_year_q3', 'NUMERIC','line'), ('ckk','next_q4', 'ckk','expenses_next_year_q4', 'NUMERIC','line'), ('ckk','rf_schedule', 'ckk','rf_schedule', 'TEXT', 'line'), ('ckk','deadline', 'ckk','delivery_deadline', 'TEXT', 'line'), ('ckk','proc_plan', 'ckk','procurement_plan', 'TEXT', 'line'), ('ckk','proc_method', 'ckk','procurement_method','TEXT', 'line'), ('ckk','comment', 'ckk','comment', 'TEXT', 'line'), ('contract_detail','counterparty', 'contract_detail','counterparty', 'TEXT', 'line'), ('contract_detail','reference', 'contract_detail','reference', 'TEXT', 'line'), ('contract_detail','addenda', 'contract_detail','addenda', 'TEXT', 'line'), ('contract_detail','subject', 'contract_detail','subject', 'TEXT', 'line'), ('contract_detail','currency', 'contract_detail','currency', 'TEXT', 'line'), ('contract_detail','ceiling', 'contract_detail','ceiling_amount','NUMERIC','line'), ('contract_detail','q1', 'contract_detail','expenses_q1', 'NUMERIC','line'), ('contract_detail','q2', 'contract_detail','expenses_q2', 'NUMERIC','line'), ('contract_detail','q3', 'contract_detail','expenses_q3', 'NUMERIC','line'), ('contract_detail','q4', 'contract_detail','expenses_q4', 'NUMERIC','line'), ('contract_detail','rf_schedule', 'contract_detail','rf_schedule', 'TEXT', 'line'), ('contract_detail','vat_rate', 'contract_detail','vat_rate', 'TEXT', 'line'), ('contract_detail','exchange_rate', 'contract_detail','exchange_rate', 'NUMERIC','line'), ('contract_detail','amount_foreign', 'contract_detail','amount_foreign','NUMERIC','line'), ('contract_detail','deadline', 'contract_detail','deadline', 'TEXT', 'line'), ('contract_detail','payment_scheme', 'contract_detail','payment_scheme','TEXT', 'line'), ('contract_detail','act', 'contract_detail','act', 'TEXT', 'line'), ('contract_detail','comment', 'contract_detail','comment', 'TEXT', 'line'), ('contract_detail','contract_date', 'contract_detail','contract_date', 'DATE', 'line'), ('contract','counterparty', 'contract_detail','counterparty', 'TEXT', 'line'), ('contract','addenda', 'contract_detail','addenda', 'TEXT', 'line'), ('contract','amount_foreign', 'contract_detail','amount_foreign','NUMERIC','line'), ('contract','q1', 'contract_detail','expenses_q1', 'NUMERIC','line'), ('contract','q2', 'contract_detail','expenses_q2', 'NUMERIC','line'), ('contract','q3', 'contract_detail','expenses_q3', 'NUMERIC','line'), ('contract','q4', 'contract_detail','expenses_q4', 'NUMERIC','line'), ('contract','rf_schedule', 'contract_detail','rf_schedule', 'TEXT', 'line'), ('contract','exchange_rate', 'contract_detail','exchange_rate', 'NUMERIC','line'), ('contract','reference', 'contract_detail','reference', 'TEXT', 'line'), ('contract','date', 'contract_detail','contract_date', 'DATE', 'line'), ('contract','subject', 'contract_detail','subject', 'TEXT', 'line'), ('contract','currency', 'contract_detail','currency', 'TEXT', 'line'), ('contract','ceiling', 'contract_detail','ceiling_amount','NUMERIC','line'), ('contract','vat_rate', 'contract_detail','vat_rate', 'TEXT', 'line'), ('contract','deadline', 'contract_detail','deadline', 'TEXT', 'line'), ('contract','scheme', 'contract_detail','payment_scheme','TEXT', 'line'), ('contract','act', 'contract_detail','act', 'TEXT', 'line'), ('contract','comment', 'contract_detail','comment', 'TEXT', 'line'), ('sequestration','q1', 'sequestration','adj_q1', 'NUMERIC','line_actor'), ('sequestration','q2', 'sequestration','adj_q2', 'NUMERIC','line_actor'), ('sequestration','q3', 'sequestration','adj_q3', 'NUMERIC','line_actor'), ('sequestration','q4', 'sequestration','adj_q4', 'NUMERIC','line_actor'), ('sequestration','justification','sequestration','justification','TEXT', 'line_actor'), ('seq_dfip','q1', 'sequestration','adj_q1', 'NUMERIC','line_actor'), ('seq_dfip','q2', 'sequestration','adj_q2', 'NUMERIC','line_actor'), ('seq_dfip','q3', 'sequestration','adj_q3', 'NUMERIC','line_actor'), ('seq_dfip','q4', 'sequestration','adj_q4', 'NUMERIC','line_actor'), ('seq_dfip','justification','sequestration','justification','TEXT', 'line_actor'), ('seq_ssp','q1', 'sequestration','adj_q1', 'NUMERIC','line_actor'), ('seq_ssp','q2', 'sequestration','adj_q2', 'NUMERIC','line_actor'), ('seq_ssp','q3', 'sequestration','adj_q3', 'NUMERIC','line_actor'), ('seq_ssp','q4', 'sequestration','adj_q4', 'NUMERIC','line_actor'), ('seq_ssp','justification','sequestration','justification','TEXT', 'line_actor'), ('q','adj_current', 'budget_line_quarter','adj_current', 'NUMERIC','line_quarter'), ('q','adj_ssp', 'budget_line_quarter','adj_ssp', 'NUMERIC','line_quarter'), ('q','adj_rf', 'budget_line_quarter','adj_rf', 'NUMERIC','line_quarter'), ('q','adj_reserve', 'budget_line_quarter','adj_reserve', 'NUMERIC','line_quarter'), ('q','adj_comment', 'budget_line_quarter','adj_comment', 'TEXT', 'line_quarter'), ('q','target_change', 'budget_line_quarter','target_change', 'NUMERIC','line_quarter'), ('q','base_correction', 'budget_line_quarter','base_plan_correction', 'NUMERIC','line_quarter'), ('q','base_correction_comment', 'budget_line_quarter','base_plan_correction_comment','TEXT', 'line_quarter'), ('q','pay_date', 'budget_line_quarter','payment_date', 'DATE', 'line_quarter'), ('q','pay_amount', 'budget_line_quarter','payment_amount', 'NUMERIC','line_quarter'), ('q','pay_ho', 'budget_line_quarter','payment_amount_ho', 'NUMERIC','line_quarter'), ('q','pay_rf', 'budget_line_quarter','payment_amount_rf', 'NUMERIC','line_quarter'), ('q','pay_comment', 'budget_line_quarter','payment_comment', 'TEXT', 'line_quarter'), ('q','pay_act', 'budget_line_quarter','payment_act', 'TEXT', 'line_quarter'), ('q','actual_m1', 'budget_line_quarter','actual_m1', 'NUMERIC','line_quarter'), ('q','actual_m2', 'budget_line_quarter','actual_m2', 'NUMERIC','line_quarter'), ('q','actual_m3', 'budget_line_quarter','actual_m3', 'NUMERIC','line_quarter'), ('q','actual_spod', 'budget_line_quarter','actual_spod', 'NUMERIC','line_quarter'), ('q','transfer_q2', 'budget_line_quarter','transfer_to_q2', 'NUMERIC','line_quarter'), ('q','transfer_q3', 'budget_line_quarter','transfer_to_q3', 'NUMERIC','line_quarter'), ('q','transfer_q4', 'budget_line_quarter','transfer_to_q4', 'NUMERIC','line_quarter'), ('q','transfer_econ', 'budget_line_quarter','transfer_to_economy', 'NUMERIC','line_quarter'), ('q','transfer_q2_delay_acts', 'budget_line_quarter','transfer_delay_acts', 'NUMERIC','line_quarter'), ('q','transfer_q2_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'), ('q','transfer_q2_economy_rf', 'budget_line_quarter','transfer_economy_rf', 'NUMERIC','line_quarter'), ('q','transfer_q3_delay_acts', 'budget_line_quarter','transfer_delay_acts', 'NUMERIC','line_quarter'), ('q','transfer_q3_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'), ('q','transfer_q3_economy_rf', 'budget_line_quarter','transfer_economy_rf', 'NUMERIC','line_quarter'), ('q','transfer_q4_delay_acts', 'budget_line_quarter','transfer_delay_acts', 'NUMERIC','line_quarter'), ('q','transfer_q4_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'), ('q','transfer_q4_economy_rf', 'budget_line_quarter','transfer_economy_rf', 'NUMERIC','line_quarter'), ('q','transfer_next_comment', 'budget_line_quarter','transfer_next_comment', 'TEXT', 'line_quarter'), ('q','transfer_far_comment', 'budget_line_quarter','transfer_far_comment', 'TEXT', 'line_quarter'), ('q','rev_eco', 'budget_line_quarter','plan_revision_eco_change','NUMERIC','line_quarter'), ('q','rev_item', 'budget_line_quarter','plan_revision_item_adj', 'NUMERIC','line_quarter'), ('q','rev_inc', 'budget_line_quarter','plan_revision_increase', 'NUMERIC','line_quarter'), ('q','rev_seq', 'budget_line_quarter','plan_revision_sequester', 'NUMERIC','line_quarter'), ('q','rev_comment', 'budget_line_quarter','plan_revision_comment', 'TEXT', 'line_quarter'), ('q','revision_inc', 'budget_line_quarter','plan_revision_increase', 'NUMERIC','line_quarter'), ('q','revision_seq', 'budget_line_quarter','plan_revision_sequester', 'NUMERIC','line_quarter'), ('q','revision_comment', 'budget_line_quarter','plan_revision_comment', 'TEXT', 'line_quarter'), ('q','booking_amount', 'budget_line_quarter','booking_amount', 'NUMERIC','line_quarter') ) AS m(scope, field, target_table, target_col, target_type, key_kind) WHERE m.scope = (CASE WHEN v_q IS NOT NULL THEN 'q' ELSE v_scope END) AND m.field = v_field; IF v_target_table IS NULL THEN IF v_field IN ('corrected_plan','new_plan','actual_quarter','booking', 'rem_booking','rem_actual','residual_after_booking', 'residual_after_actual','economy','total','year') THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; RAISE EXCEPTION 'unknown_column: %', p_column; END IF; IF v_key_kind = 'line' THEN v_sql := format( 'INSERT INTO v3.%1$I (line_id, %2$I) VALUES ($1, ($2 #>> ''{}'')::%3$s) ' || 'ON CONFLICT (line_id) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col, v_target_type ); EXECUTE v_sql USING p_line_id, p_value; ELSIF v_key_kind = 'line_quarter' THEN IF v_q IS NULL THEN RAISE EXCEPTION 'internal: quarter not set for %', p_column; END IF; v_sql := format( 'INSERT INTO v3.%1$I (line_id, quarter, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) ' || 'ON CONFLICT (line_id, quarter) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col, v_target_type ); EXECUTE v_sql USING p_line_id, v_q, p_value; ELSIF v_key_kind = 'line_actor' THEN IF v_actor IS NULL THEN RAISE EXCEPTION 'internal: actor not set for %', p_column; END IF; v_sql := format( 'INSERT INTO v3.%1$I (line_id, actor, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) ' || 'ON CONFLICT (line_id, actor) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col, v_target_type ); EXECUTE v_sql USING p_line_id, v_actor, p_value; ELSE RAISE EXCEPTION 'internal: unknown key_kind %', v_key_kind; END IF; UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id; END; $function$ ; UPDATE v3.phase_template SET closes_at = closes_at - INTERVAL '2026 years', opens_at = opens_at - INTERVAL '2026 years';