DFiP_Budget_planing/api/alembic/versions/sql/0018_technical_number.sql

711 lines
40 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ════════════════════════════════════════════════════════════════════════════
-- МИГРАЦИЯ: Автогенерация 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$
;