-- ════════════════════════════════════════════════════════════════════════════ -- МИГРАЦИЯ: Автогенерация technical_number + запрет ручного редактирования -- Формат: YYMM_<код РФ/ВСП 4 цифры><порядковый счётчик 2 цифры> -- Пример: 2606_180801 (26 — год, 06 — месяц, 1808 — org_unit id, 01 — счётчик) -- ════════════════════════════════════════════════════════════════════════════ -- Уникальность технического номера проекта CREATE UNIQUE INDEX IF NOT EXISTS uq_v3_project_technical_number ON v3.project (technical_number); -- add_project: генерируем technical_number автоматически при создании проекта 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; v_seq INT; v_technical_number VARCHAR; 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 ou WHERE id = p_org_unit_id AND ou.is_active) 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; -- Порядковый счётчик проекта для данного РФ/ВСП (org_unit) SELECT COALESCE(MAX(right(technical_number, 2)::int), 0) + 1 INTO v_seq FROM v3.project WHERE org_unit_id = p_org_unit_id AND technical_number IS NOT NULL; v_technical_number := to_char(now(), 'YYMM') || '_' || lpad(p_org_unit_id::text, 4, '0') || lpad(v_seq::text, 2, '0'); 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, technical_number ) 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, v_technical_number ) RETURNING id INTO v_pid; -- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей. 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, 'technical_number', v_technical_number, '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$ ; -- upd_project: убираем параметр technical_number (редактировать больше нельзя) DROP FUNCTION IF EXISTS v3.upd_project(integer, varchar, varchar, varchar, varchar, varchar, varchar, varchar, integer, numeric, integer, date, date, date, date, varchar); CREATE OR REPLACE FUNCTION v3.upd_project( p_project_id integer, p_name character varying DEFAULT NULL, p_status character varying DEFAULT NULL, p_project_type character varying DEFAULT NULL, p_vsp_format character varying DEFAULT NULL, p_placement_type character varying DEFAULT NULL, p_object_address character varying DEFAULT NULL, p_staff_count integer DEFAULT NULL, p_total_area numeric DEFAULT NULL, p_org_unit_id integer DEFAULT NULL, p_krf_decision_date date DEFAULT NULL, p_fk_decision_date date DEFAULT NULL, p_board_decision_date date DEFAULT NULL, p_open_relocate_close_date date DEFAULT NULL, p_funding_by_ko_decision character varying DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE v_old v3.project%ROWTYPE; v_changes JSONB := '{}'::jsonb; BEGIN SELECT * INTO v_old FROM v3.project WHERE id = p_project_id; IF NOT FOUND THEN RAISE EXCEPTION 'project id=% не существует', p_project_id; END IF; IF p_name IS NOT NULL AND p_name IS DISTINCT FROM v_old.name THEN UPDATE v3.project SET name = p_name WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('name', jsonb_build_object('before', v3._diff_val(v_old.name), 'after', v3._diff_val(p_name))); END IF; IF p_status IS NOT NULL AND p_status IS DISTINCT FROM v_old.status THEN UPDATE v3.project SET status = p_status WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('before', v3._diff_val(v_old.status), 'after', v3._diff_val(p_status))); END IF; IF p_project_type IS NOT NULL AND p_project_type IS DISTINCT FROM v_old.project_type THEN UPDATE v3.project SET project_type = p_project_type WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('project_type', jsonb_build_object('before', v3._diff_val(v_old.project_type), 'after', v3._diff_val(p_project_type))); END IF; IF p_vsp_format IS NOT NULL AND p_vsp_format IS DISTINCT FROM v_old.vsp_format THEN UPDATE v3.project SET vsp_format = p_vsp_format WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('vsp_format', jsonb_build_object('before', v3._diff_val(v_old.vsp_format), 'after', v3._diff_val(p_vsp_format))); END IF; IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN UPDATE v3.project SET placement_type = p_placement_type WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('placement_type', jsonb_build_object('before', v3._diff_val(v_old.placement_type), 'after', v3._diff_val(p_placement_type))); END IF; IF p_object_address IS NOT NULL AND p_object_address IS DISTINCT FROM v_old.object_address THEN UPDATE v3.project SET object_address = p_object_address WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('object_address', jsonb_build_object('before', v3._diff_val(v_old.object_address), 'after', v3._diff_val(p_object_address))); END IF; IF p_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN UPDATE v3.project SET staff_count = p_staff_count WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('staff_count', jsonb_build_object('before', v3._diff_val(v_old.staff_count), 'after', v3._diff_val(p_staff_count))); END IF; IF p_total_area IS NOT NULL AND p_total_area IS DISTINCT FROM v_old.total_area THEN UPDATE v3.project SET total_area = p_total_area WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('total_area', jsonb_build_object('before', v3._diff_val(v_old.total_area), 'after', v3._diff_val(p_total_area))); END IF; IF p_org_unit_id IS NOT NULL AND p_org_unit_id IS DISTINCT FROM v_old.org_unit_id THEN 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; UPDATE v3.project SET org_unit_id = p_org_unit_id WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('org_unit_id', jsonb_build_object('before', v3._diff_val(v_old.org_unit_id), 'after', v3._diff_val(p_org_unit_id))); END IF; IF p_krf_decision_date IS NOT NULL AND p_krf_decision_date IS DISTINCT FROM v_old.krf_decision_date THEN UPDATE v3.project SET krf_decision_date = p_krf_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('krf_decision_date', jsonb_build_object('before', v3._diff_val(v_old.krf_decision_date), 'after', v3._diff_val(p_krf_decision_date))); END IF; IF p_fk_decision_date IS NOT NULL AND p_fk_decision_date IS DISTINCT FROM v_old.fk_decision_date THEN UPDATE v3.project SET fk_decision_date = p_fk_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('fk_decision_date', jsonb_build_object('before', v3._diff_val(v_old.fk_decision_date), 'after', v3._diff_val(p_fk_decision_date))); END IF; IF p_board_decision_date IS NOT NULL AND p_board_decision_date IS DISTINCT FROM v_old.board_decision_date THEN UPDATE v3.project SET board_decision_date = p_board_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('board_decision_date', jsonb_build_object('before', v3._diff_val(v_old.board_decision_date), 'after', v3._diff_val(p_board_decision_date))); END IF; IF p_open_relocate_close_date IS NOT NULL AND p_open_relocate_close_date IS DISTINCT FROM v_old.open_relocate_close_date THEN UPDATE v3.project SET open_relocate_close_date = p_open_relocate_close_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('open_relocate_close_date', jsonb_build_object('before', v3._diff_val(v_old.open_relocate_close_date), 'after', v3._diff_val(p_open_relocate_close_date))); END IF; IF p_funding_by_ko_decision IS NOT NULL AND p_funding_by_ko_decision IS DISTINCT FROM v_old.funding_by_ko_decision THEN UPDATE v3.project SET funding_by_ko_decision = p_funding_by_ko_decision WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('funding_by_ko_decision', jsonb_build_object('before', v3._diff_val(v_old.funding_by_ko_decision), 'after', v3._diff_val(p_funding_by_ko_decision))); END IF; IF v_changes <> '{}'::jsonb THEN PERFORM v3.log_event( 'PROJECT_UPDATE', 'PROJECT', jsonb_build_object( 'project_id', p_project_id, 'entity_id', p_project_id, 'changes', v_changes ), null, null, COALESCE(p_org_unit_id, v_old.org_unit_id) ); END IF; END; $function$ ; UPDATE v3.phase_template SET column_keys = '{plan.q1,plan.q2,plan.q3,plan.q4,plan.year,plan.comment,contract_summary.total,contract_summary.reference,contract_summary.counterparty,contract_summary.deadline,contract_summary.comment,contract_summary.future_y1,contract_summary.future_y2,contract_summary.other_ssp,contract_summary.centralized_flag,allocation.contract_ref,allocation.allocation_purpose,seq_dfip.q1,seq_dfip.q2,seq_dfip.q3,seq_dfip.q4,seq_dfip.year,seq_dfip.justification,reserve.q1,reserve.q2,reserve.q3,reserve.q4,reserve.year,reserve.justification,approved.q1,approved.q2,approved.q3,approved.q4,approved.year,collegial.approved,collegial.protocol,collegial.note,ckk.ceiling,ckk.q1,ckk.q2,ckk.q3,ckk.q4,ckk.rf_schedule,ckk.deadline,ckk.proc_plan,ckk.proc_method,ckk.comment,contract.counterparty,contract.reference,contract.addenda,contract.subject,contract.currency,contract.ceiling,contract.q1,contract.q2,contract.q3,contract.q4,contract.rf_schedule,contract.vat_rate,contract.exchange_rate,contract.amount_foreign,contract.deadline,contract.scheme,contract.act,contract.comment,q1.adj_current,q1.adj_ssp,q1.adj_rf,q1.adj_reserve,q1.adj_comment,q1.corrected_plan,q1.pay_date,q1.pay_amount,q1.pay_ho,q1.pay_rf,q1.pay_comment,q1.pay_act,q1.booking,q1.actual_m1,q1.actual_m2,q1.actual_m3,q1.actual_quarter,q1.residual_after_booking,q1.residual_after_actual,q1.transfer_q2,q1.transfer_q3,q1.transfer_q4,q1.transfer_econ,q1.total,q2.target_change,q2.base_correction,q2.revision_inc,q2.revision_seq,q2.revision_comment,q2.new_plan,q2.adj_current,q2.adj_ssp,q2.adj_rf,q2.adj_reserve,q2.corrected_plan,q2.pay_date,q2.pay_amount,q2.pay_ho,q2.pay_rf,q2.pay_comment,q2.pay_act,q2.booking,q2.actual_m1,q2.actual_m2,q2.actual_m3,q2.actual_quarter,q2.residual_after_booking,q2.residual_after_actual,q2.transfer_q3,q2.transfer_q4,q2.transfer_econ,q2.total,q3.target_change,q3.base_correction,q3.revision_inc,q3.revision_seq,q3.revision_comment,q3.new_plan,q3.adj_current,q3.adj_ssp,q3.adj_rf,q3.adj_reserve,q3.corrected_plan,q3.pay_date,q3.pay_amount,q3.pay_ho,q3.pay_rf,q3.pay_comment,q3.pay_act,q3.booking,q3.actual_m1,q3.actual_m2,q3.actual_m3,q3.actual_quarter,q3.residual_after_booking,q3.residual_after_actual,q3.transfer_q4,q3.transfer_econ,q3.total,q4.target_change,q4.base_correction,q4.revision_inc,q4.revision_seq,q4.revision_comment,q4.new_plan,q4.adj_current,q4.adj_ssp,q4.adj_rf,q4.adj_reserve,q4.corrected_plan,q4.pay_date,q4.pay_amount,q4.pay_ho,q4.pay_rf,q4.pay_comment,q4.pay_act,q4.booking,q4.actual_m1,q4.actual_m2,q4.actual_m3,q4.actual_quarter,q4.actual_spod,q4.residual_after_booking,q4.residual_after_actual,q4.transfer_econ,q4.total,totals.fact_year}' WHERE form_type = 'FORM_4'; ALTER TABLE v3.form_phase DROP CONSTRAINT chk_v4_form_phase_no_admin; -- DROP FUNCTION v3._apply_form_cell(int4, text, jsonb, varchar, int4); 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_form_type 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 SELECT form_type_code INTO v_form_type from v3.budget_form where id = p_form_id; IF v_form_type = 'FORM_4' THEN RAISE EXCEPTION 'Нельзя редактировать поле name'; END IF; 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 IF v_target_type = 'DATE' THEN v_sql := format( 'INSERT INTO v3.%1$I (line_id, %2$I) VALUES ($1, to_date($2->>0, ''DD.MM.YYYY'')) ' || 'ON CONFLICT (line_id) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col ); ELSE 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 ); END IF; 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; IF v_target_type = 'DATE' THEN v_sql := format( 'INSERT INTO v3.%1$I (line_id, quarter, %2$I) VALUES ($1, $2, to_date($3->>0, ''DD.MM.YYYY'')) ' || 'ON CONFLICT (line_id, quarter) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col ); ELSE 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 ); END IF; 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; IF v_target_type = 'DATE' THEN v_sql := format( 'INSERT INTO v3.%1$I (line_id, actor, %2$I) VALUES ($1, $2, to_date($3->>0, ''DD.MM.YYYY'')) ' || 'ON CONFLICT (line_id, actor) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col, v_target_type ); ELSE 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 ); END IF; 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$ ;