form1-sub_sub-fix #171

Merged
tsygankoviva merged 2 commits from form1-sub_sub-fix into test 2026-09-22 17:20:40 +03:00
4 changed files with 397 additions and 458 deletions

View File

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

View File

@ -1301,8 +1301,8 @@ BEGIN
IF v_sequence_name IS NOT NULL THEN
PERFORM setval(
v_sequence_name,
COALESCE((SELECT MAX(id) FROM v3.budget_line), 0),
true
COALESCE((SELECT MAX(id) FROM v3.budget_line), 1),
(SELECT COUNT(*) > 0 FROM v3.budget_line)
);
END IF;
END;

View File

@ -2510,6 +2510,7 @@ BEGIN
JOIN v3.expense_item ei ON ei.id = l.expense_item_id
JOIN tw t ON t.id = l.expense_item_id
WHERE l.rf_project_report_id = p_report_id
AND cardinality(t.desc_ids) = 1
),
-- Поквартальные данные
jq AS (
@ -2919,6 +2920,13 @@ BEGIN
WHERE expense_item_id = p_expense_item_id AND form_type_code = 'FORM_3') THEN
RAISE EXCEPTION 'expense_item id=% не привязан к form_type FORM_3', p_expense_item_id;
END IF;
IF COALESCE((
SELECT cardinality(t.desc_ids) > 1
FROM v3.mv_expense_item_tree t
WHERE t.id = p_expense_item_id
), false) THEN
RAISE EXCEPTION 'Нельзя создать строку по expense_item id=%: у статьи есть дочерние статьи', p_expense_item_id;
END IF;
v_user_id := v3.current_user_id();
v_decision := v3.can_edit3(p_report_id, NULL, v_user_id);
@ -3833,141 +3841,54 @@ $function$
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
CREATE OR REPLACE FUNCTION v3.add_budget_line(p_form_id integer, p_expense_item_id integer DEFAULT NULL::integer, p_sheet character varying DEFAULT NULL::character varying, p_item_id character varying DEFAULT NULL::character varying, p_section_code character varying DEFAULT NULL::character varying, p_direction character varying DEFAULT NULL::character varying, p_name character varying DEFAULT NULL::character varying, p_internal_order character varying DEFAULT NULL::character varying, p_vsp_id integer DEFAULT NULL::integer, p_project_id integer DEFAULT NULL::integer, p_justification character varying DEFAULT NULL::character varying, p_contract_number character varying DEFAULT NULL::character varying, p_contract_end_date date DEFAULT NULL::date)
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
LANGUAGE plpgsql
AS $function$
DECLARE
v_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_form_type VARCHAR;
v_eid INT;
v_sheet VARCHAR;
v_auto_dir VARCHAR;
v_final_dir VARCHAR;
v_new_id INT;
v_cnt INT;
v_sat_table TEXT;
v_sat_eid INT;
v_parent_line INT;
v_decision JSONB;
v_msg TEXT;
v_user_id INT;
v_line_name VARCHAR;
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;
SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id;
IF v_form_type IS NULL THEN
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
END IF;
IF v_form_type = 'FORM_3' THEN
RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)';
END IF;
IF p_sheet = 'AHR_LIMIT' THEN
IF p_form_id IS NULL THEN
RAISE EXCEPTION 'internal: p_form_id не передан для AHR_LIMIT';
END IF;
v_user_id := v3.current_user_id();
v_decision := v3.can_edit(p_form_id, p_sheet, p_direction, NULL, v_user_id);
IF NOT 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;
IF NOT (v_decision->>'ok')::boolean THEN
IF v_decision->>'code' = 'role_not_allowed' THEN
RAISE EXCEPTION 'role_not_allowed: Невозможно вставить строку';
ELSIF v_decision->>'code' = 'org_not_assigned' THEN
RAISE EXCEPTION 'org_not_assigned: Невозможно вставить строку';
ELSIF v_decision->>'code' = 'window_closed' THEN
IF v_decision ? 'opens_at' THEN
v_msg := format('window_closed: opens %s', v_decision->>'opens_at');
ELSIF v_decision->>'closes_at' IS NOT NULL THEN
v_msg := format('window_closed: closed %s', v_decision->>'closes_at');
ELSE
v_msg := 'window_closed: no phase';
END IF;
RAISE EXCEPTION '%', v_msg;
ELSE
RAISE EXCEPTION '%', v_decision->>'code';
END IF;
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;
IF (v_target_type = 'NUMERIC' OR v_target_type = 'INTEGER') AND p_value = '""'::jsonb THEN
p_value := '"0"'::jsonb;
END IF;
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
@ -3976,347 +3897,155 @@ BEGIN
WHEN 'AHR_UTILITY' THEN 'utility_detail'
WHEN 'AHR_SECURITY' THEN 'security_detail'
END;
v_sat_eid := CASE p_sheet
WHEN 'AHR_RENT' THEN 293
WHEN 'AHR_UTILITY' THEN 799
WHEN 'AHR_SECURITY' THEN 31
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;
IF p_vsp_id IS NULL THEN
RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet;
END IF;
IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
END IF;
IF v_target_type = 'NUMERIC' AND p_value = '""'::jsonb THEN
p_value := '"0"'::jsonb;
SELECT id INTO v_parent_line
FROM v3.budget_line
WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid
LIMIT 1;
IF v_parent_line IS NULL THEN
INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction)
VALUES (
p_form_id, v_sat_eid,
CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END
) RETURNING id INTO v_parent_line;
END IF;
EXECUTE format(
'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;
'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) '
|| 'VALUES ($1, $2, $3, $4) RETURNING id',
v_sat_table
) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date
INTO v_new_id;
PERFORM v3.sync_form2_ahr_satellite_line(p_form_id, p_sheet, v_new_id);
RETURN QUERY
SELECT v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id;
RETURN;
END IF;
IF p_expense_item_id IS NOT NULL THEN
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
FROM v3.expense_item ei WHERE ei.id = p_expense_item_id;
IF v_eid IS NULL THEN
RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
END IF;
IF NOT EXISTS (
SELECT 1 FROM v3.expense_item_form_type
WHERE expense_item_id = v_eid AND form_type_code = v_form_type
) THEN
RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type;
END IF;
ELSE
IF p_sheet IS NULL OR p_item_id IS NULL THEN
RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)';
END IF;
SELECT count(*) INTO v_cnt
FROM v3.expense_item ei
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
WHERE ei.item_id = p_item_id
AND ei.sheet = p_sheet
AND eift.form_type_code = v_form_type
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
GET DIAGNOSTICS v_cnt = ROW_COUNT;
IF v_cnt = 0 THEN
RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_line_id;
RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%',
p_sheet, p_item_id, p_section_code, p_direction, v_form_type;
END IF;
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_target_type = 'NUMERIC' AND p_value = '""'::jsonb THEN
p_value := '"0"'::jsonb;
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
);
IF v_cnt > 1 THEN
RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую',
v_cnt, p_sheet, p_item_id, p_section_code, p_direction;
END IF;
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;
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
FROM v3.expense_item ei
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
WHERE ei.item_id = p_item_id
AND ei.sheet = p_sheet
AND eift.form_type_code = v_form_type
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
END IF;
UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id;
v_auto_dir := CASE
WHEN v_form_type = 'FORM_2' THEN 'Support'
WHEN v_form_type = 'FORM_4' THEN 'Development'
WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL
ELSE NULL
END;
IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN
RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet;
END IF;
IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN
RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)',
p_direction, v_form_type, v_auto_dir;
END IF;
IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN
RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction;
END IF;
v_final_dir := COALESCE(p_direction, v_auto_dir);
IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
END IF;
IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.form4_project WHERE id = p_project_id) THEN
RAISE EXCEPTION 'form4_project id=% не существует', p_project_id;
END IF;
v_line_name := CASE WHEN v_form_type = 'FORM_4' THEN NULL ELSE '' END;
INSERT INTO v3.budget_line (
budget_form_id, expense_item_id, name, internal_order,
vsp_id, project_id, direction, justification
) VALUES (
p_form_id, v_eid, v_line_name, p_internal_order,
p_vsp_id, p_project_id, v_final_dir, p_justification
) RETURNING id INTO v_new_id;
PERFORM v3.log_event(
'ROW_CREATE', 'ROW',
jsonb_build_object(
'sheet', v_sheet,
'line_id', v_new_id,
'expense_item_id', v_eid,
'direction', v_final_dir,
'name', v_line_name,
'vsp_id', p_vsp_id,
'project_id', p_project_id
),
p_form_id
);
DECLARE v_ei_path INT[];
BEGIN
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
RETURN QUERY
SELECT v.row_type, v.depth, v.sort_order, v.data
FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
ORDER BY v.sort_order;
END;
END;
$function$
;
$function$;

View File

@ -0,0 +1,195 @@
-- Fix sort.docx items 1-2:
-- - AHR RF depreciation and 5.00.0 tax block order.
-- - FORM_3 R061012024 order between R061012023 and R061012121.
-- - FORM_4 SMETA excludes sections absent from the project estimate template.
-- v3.mv_expense_item_tree sorts by expense_item.id path, so the catalog ids
-- must be renumbered together with all foreign-key references.
ALTER TABLE v3.budget_line DROP CONSTRAINT IF EXISTS budget_line_expense_item_fk;
ALTER TABLE v3.expense_item DROP CONSTRAINT IF EXISTS expense_item_expense_item_fk;
ALTER TABLE v3.expense_item_form_type DROP CONSTRAINT IF EXISTS expense_item_form_type_expense_item_fk;
ALTER TABLE v3.fixed_asset_report DROP CONSTRAINT IF EXISTS fixed_asset_report_expense_item_fk;
ALTER TABLE v3.rf_project_report_line DROP CONSTRAINT IF EXISTS rf_project_report_line_expense_item_fk;
CREATE TEMP TABLE _expense_item_id_map (
old_id INT PRIMARY KEY,
new_id INT NOT NULL UNIQUE
) ON COMMIT DROP;
INSERT INTO _expense_item_id_map (old_id, new_id)
SELECT id, id + 20
FROM v3.expense_item
WHERE id BETWEEN 82 AND 94;
INSERT INTO _expense_item_id_map (old_id, new_id)
SELECT id, id + 21
FROM v3.expense_item
WHERE id BETWEEN 95 AND 745;
INSERT INTO _expense_item_id_map (old_id, new_id) VALUES
(746, 82),
(747, 83),
(752, 84),
(758, 85),
(754, 86),
(753, 87),
(757, 88),
(750, 89),
(756, 90),
(759, 91),
(748, 92),
(751, 93),
(749, 94),
(755, 95),
(760, 96),
(764, 97),
(762, 98),
(763, 99),
(765, 100),
(761, 101),
(775, 115),
(766, 775),
(772, 767),
(773, 768),
(771, 769),
(774, 770),
(767, 771),
(770, 772),
(768, 773),
(769, 774);
DO $block$
DECLARE
v_missing TEXT;
BEGIN
SELECT string_agg(expected.item_id, ', ' ORDER BY expected.item_id)
INTO v_missing
FROM (
VALUES
('1.16.0.'), ('1.16.1.'), ('1.16.2.'),
('R061161001'), ('R061161005'), ('R061161002'), ('R061161004'),
('R061161003'), ('R061161006'), ('R061161010'), ('R061161007'),
('R061161008'), ('R061161009'), ('R061161011'), ('R061161019'),
('R061162021'), ('R061162022'), ('R061162023'), ('R061162024'),
('R061162025'),
('R061012023'), ('R061012024'), ('R061012121'),
('5.00.0.'),
('R065010000'), ('R065020001'), ('R065030012'), ('R065040000'),
('R065050005'), ('R065070000'), ('R065090000'), ('R065100000')
) AS expected(item_id)
LEFT JOIN v3.expense_item ei
ON ei.item_id = expected.item_id
AND ei.sheet = 'AHR'
AND ei.direction = 'Support'
WHERE ei.id IS NULL;
IF v_missing IS NOT NULL THEN
RAISE EXCEPTION 'Missing AHR Support expense items for sort fix: %', v_missing;
END IF;
END;
$block$;
UPDATE v3.expense_item
SET id = -id
WHERE id IN (SELECT old_id FROM _expense_item_id_map);
UPDATE v3.expense_item ei
SET id = m.new_id
FROM _expense_item_id_map m
WHERE ei.id = -m.old_id;
UPDATE v3.expense_item ei
SET parent_id = m.new_id
FROM _expense_item_id_map m
WHERE ei.parent_id = m.old_id;
UPDATE v3.budget_line
SET expense_item_id = -expense_item_id
WHERE expense_item_id IN (SELECT old_id FROM _expense_item_id_map);
UPDATE v3.budget_line bl
SET expense_item_id = m.new_id
FROM _expense_item_id_map m
WHERE bl.expense_item_id = -m.old_id;
UPDATE v3.expense_item_form_type
SET expense_item_id = -expense_item_id
WHERE expense_item_id IN (SELECT old_id FROM _expense_item_id_map);
UPDATE v3.expense_item_form_type eft
SET expense_item_id = m.new_id
FROM _expense_item_id_map m
WHERE eft.expense_item_id = -m.old_id;
UPDATE v3.fixed_asset_report
SET expense_item_id = -expense_item_id
WHERE expense_item_id IN (SELECT old_id FROM _expense_item_id_map);
UPDATE v3.fixed_asset_report far
SET expense_item_id = m.new_id
FROM _expense_item_id_map m
WHERE far.expense_item_id = -m.old_id;
UPDATE v3.rf_project_report_line
SET expense_item_id = -expense_item_id
WHERE expense_item_id IN (SELECT old_id FROM _expense_item_id_map);
UPDATE v3.rf_project_report_line rprl
SET expense_item_id = m.new_id
FROM _expense_item_id_map m
WHERE rprl.expense_item_id = -m.old_id;
DELETE FROM v3.expense_item_form_type
WHERE form_type_code = 'FORM_4'
AND expense_item_id IN (
103, 104, 106, 107,
500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512,
513, 514, 515, 516, 517,
518, 519, 520, 521, 522,
525, 526, 527, 528,
547, 548, 549, 550, 551, 552, 553, 554, 555,
556, 557, 558,
734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746,
748,
755, 756, 757, 758, 759, 760, 766,
800, 801, 802, 803, 804, 805, 806, 807, 808
);
DO $block$
DECLARE
v_function_definition TEXT;
BEGIN
SELECT pg_get_functiondef('v3.v_summary_view(integer, character varying, integer[])'::regprocedure)
INTO v_function_definition;
v_function_definition := REPLACE(
v_function_definition,
'ROW_NUMBER() OVER (ORDER BY t.item_id)',
'ROW_NUMBER() OVER (ORDER BY t.path)'
);
EXECUTE v_function_definition;
END;
$block$;
ALTER TABLE v3.expense_item
ADD CONSTRAINT expense_item_expense_item_fk
FOREIGN KEY (parent_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.expense_item_form_type
ADD CONSTRAINT expense_item_form_type_expense_item_fk
FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.budget_line
ADD CONSTRAINT budget_line_expense_item_fk
FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.fixed_asset_report
ADD CONSTRAINT fixed_asset_report_expense_item_fk
FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.rf_project_report_line
ADD CONSTRAINT rf_project_report_line_expense_item_fk
FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
REFRESH MATERIALIZED VIEW v3.mv_expense_item_tree;