-- DROP FUNCTION v3._apply_form3_cell(int4, text, jsonb); CREATE OR REPLACE FUNCTION v3._apply_form3_cell(p_line_id integer, p_column text, p_value jsonb) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE v_parts TEXT[]; v_scope TEXT; v_field TEXT; v_q SMALLINT; v_target_col TEXT; v_target_type TEXT; v_sql TEXT; BEGIN v_parts := string_to_array(p_column, '.'); 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 = 'totals' OR v_field IN ('total_corr','quarter_actual','economy','carryover') THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; IF v_scope NOT IN ('q1','q2','q3','q4') THEN RAISE EXCEPTION 'unknown_scope: % (FORM_3 supports only q1..q4)', v_scope; END IF; v_q := substring(v_scope FROM 2)::SMALLINT; -- Маппинг JSONB-ключ → колонка rf_project_report_quarter CASE v_field WHEN 'adj_by_items' THEN v_target_col := 'adj_by_items'; v_target_type := 'NUMERIC'; WHEN 'adj_increase' THEN v_target_col := 'adj_increase'; v_target_type := 'NUMERIC'; WHEN 'm1' THEN v_target_col := 'actual_m1'; v_target_type := 'NUMERIC'; WHEN 'm2' THEN v_target_col := 'actual_m2'; v_target_type := 'NUMERIC'; WHEN 'm3' THEN v_target_col := 'actual_m3'; v_target_type := 'NUMERIC'; WHEN 'spod' THEN IF v_q <> 4 THEN RAISE EXCEPTION 'spod is q4-only: %', p_column; END IF; v_target_col := 'actual_spod'; v_target_type := 'NUMERIC'; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END CASE; v_sql := format( 'INSERT INTO v3.rf_project_report_quarter (rf_project_report_line_id, quarter, %1$I) ' || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) ' || 'ON CONFLICT (rf_project_report_line_id, quarter) DO UPDATE SET %1$I = EXCLUDED.%1$I', v_target_col, v_target_type ); EXECUTE v_sql USING p_line_id, v_q, p_value; END; $function$ ; -- 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_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, '.'); -- ═══ OTCH9F (fixed_asset_report) ══════════════════════════════════════ -- p_line_id = fixed_asset_report.id. -- Editable: b_604.{acquired,disposed}_*, b_60415.{acquired,transferred}_*, -- totals.go_balance_only; -- b_604.opening_*, b_60415.opening_* — ТОЛЬКО для month=1 (для -- месяцев 2..12 это computed накопительно из месяца 1). -- RO: equipment_name (нормативная номенклатура), month/expense_item_id/ -- item_id/section_code (ключ строки), totals.{acquired_total, -- disposed_total, balance_qty, balance_amt} (computed). 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; -- Одноуровневые ключи — только нормативные/ключевые → RO 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; -- Двухуровневые: totals.x / b_604.x / b_60415.x 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 -- opening_* для месяцев 2..12 — computed IF v_parts[2] LIKE 'opening_%' AND v_far_month <> 1 THEN RAISE EXCEPTION 'computed_field: % (opening для month>1 вычисляется из base + накопит. дельты)', p_column; END IF; -- Маппинг b_604/60415 → колонки таблицы 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; -- ═══ AHR_LIMIT (limit_template + form_limit) ═════════════════════════ -- p_line_id здесь = limit_template.id LEAF (глобальный каталог нормативов). -- Editable: qty_q1..q4 (INTEGER), comment (TEXT) — пишутся в v3.form_limit -- (UPSERT по (budget_form_id, template_id)). -- amount_q1..q4 — computed по формуле в read (qty × limit × period_factor). -- name/unit/limit_*/section_no/expense_item_code — нормативный справочник -- (правится отдельным админ-API, не через write UI). IF p_sheet = 'AHR_LIMIT' THEN IF p_form_id IS NULL THEN RAISE EXCEPTION 'internal: p_form_id не передан для AHR_LIMIT'; END IF; -- Sanity: строка должна быть LEAF 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; -- ═══ Сателлиты (AHR_RENT / AHR_UTILITY / AHR_SECURITY) ═══════════════ -- Здесь p_line_id трактуется как id строки сателлита (rent/utility/ -- security_detail.id) — НЕ budget_line.id. UPDATE по этому id; INSERT/ -- DELETE — через add_budget_line / del_budget_line с тем же sheet. 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; -- ═══ Основная сетка (FORM_1/2/4 AHR/CAP/OPER) ════════════════════════ 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]; -- ── header: editable budget_line columns ────────────────────────────── IF v_scope = 'header' THEN IF v_field = 'name' THEN UPDATE v3.budget_line SET name = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id; ELSIF v_field = 'internal_order' THEN UPDATE v3.budget_line SET internal_order = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id; ELSIF v_field = '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; -- ── computed scopes (RO) ────────────────────────────────────────────── IF v_scope IN ('approved','totals') THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; -- ── booking.y{2026|2027}.qN → v3.ckk (см. read FORM_2/4 jsonb) ──────── -- В read-обёртках booking синтезируется из ckk.expenses_qN (= y2026, -- текущий год) и ckk.expenses_next_year_qN (= y2027, следующий). -- Имена y2026/y2027 захардкожены в read; повторяем то же в write. 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; -- ── Маппинг (scope, field) → (table, col, type, key_kind, actor) ───── -- key_kind: 'line' — PK/UQ = (line_id) -- 'line_quarter'— PK = (line_id, quarter), quarter из v_q (берётся из scope qN) -- 'line_actor' — UQ = (line_id, actor), actor из v_actor -- -- VALUES — это и есть white-list редактируемых полей. Поля, отсутствующие -- здесь (computed: corrected_plan, new_plan, actual_quarter, rem_*, economy, -- total, booking; FORM_2 q1.adj_rf — нет такого поля у FORM_2; и т.п.) → -- падают с unknown_column ниже. -- Quarter scope qN → ставим v_q и нормализуем scope в 'q' для маппинга IF v_scope IN ('q1','q2','q3','q4') THEN v_q := substring(v_scope FROM 2)::SMALLINT; ELSE v_q := NULL; END IF; -- Actor для sequestration scopes 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 ────────────────────────────────────────────────────────── ('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 ─────────────────────────────────────────────────────── ('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 (FORM_1/4) ───────────────────────────────────────── ('allocation','order', 'allocation','internal_order','TEXT','line'), ('allocation','property', 'allocation','property_object','TEXT','line'), -- ─── contract_summary (FORM_1/4) ─────────────────────────────────── ('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'), -- ─── collegial_approval (FORM_1/4) ───────────────────────────────── ('collegial','approved', 'collegial_approval','approved_amount', 'NUMERIC','line'), ('collegial','protocol', 'collegial_approval','protocol_reference','TEXT', 'line'), ('collegial','note', 'collegial_approval','note', 'TEXT', 'line'), -- ─── ckk (FORM_1/4) ──────────────────────────────────────────────── ('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 (FORM_1) и его alias contract (FORM_2/4) ───── ('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','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 (line_id, actor) — три алиаса scope ──────────── ('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'), -- ─── budget_line_quarter — все 4 квартала через нормализованный scope='q' ─ -- (v_scope qN, v_q заполнен; маппинг ниже использует scope='q') ('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'), -- разбивка переноса в следующий квартал (transfer_q{N+1}_*) — единые поля, -- индекс N+1 в имени ключа для UI смыслово важен, в БД одно и то же поле: ('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'), -- plan_revision_* — две группы алиасов: FORM_1 (rev_*) и FORM_2/4 (revision_*). ('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 -- Распознаваемые computed-ключи отдельно — для понятной ошибки: 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; -- Извлекаем скаляр из JSONB — '#>> {}' возвращает NULL для jsonb null -- Cast прицельный (NUMERIC/TEXT/DATE/INT) делается в dynamic SQL ниже. -- ── Dynamic UPSERT ──────────────────────────────────────────────────── IF v_key_kind = 'line' THEN v_sql := format( 'INSERT INTO v3.%1$I (line_id, %2$I) VALUES ($1, ($2 #>> ''{}'')::%3$s) ' || 'ON CONFLICT (line_id) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col, v_target_type ); EXECUTE v_sql USING p_line_id, p_value; ELSIF v_key_kind = 'line_quarter' THEN IF v_q IS NULL THEN RAISE EXCEPTION 'internal: quarter not set for %', p_column; END IF; v_sql := format( 'INSERT INTO v3.%1$I (line_id, quarter, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) ' || 'ON CONFLICT (line_id, quarter) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col, v_target_type ); EXECUTE v_sql USING p_line_id, v_q, p_value; ELSIF v_key_kind = 'line_actor' THEN IF v_actor IS NULL THEN RAISE EXCEPTION 'internal: actor not set for %', p_column; END IF; -- Для quarter-полей sequestration: scope.field=qN→adj_qN, justification без quarter v_sql := format( 'INSERT INTO v3.%1$I (line_id, actor, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) ' || 'ON CONFLICT (line_id, actor) DO UPDATE SET %2$I = EXCLUDED.%2$I', v_target_table, v_target_col, v_target_type ); EXECUTE v_sql USING p_line_id, v_actor, p_value; ELSE RAISE EXCEPTION 'internal: unknown key_kind %', v_key_kind; END IF; -- Помечаем budget_line как изменённую (для updated_at iteration) UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id; END; $function$ ; -- DROP FUNCTION v3._diff_val(anyelement); CREATE OR REPLACE FUNCTION v3._diff_val(p_val anyelement) RETURNS jsonb LANGUAGE sql IMMUTABLE AS $function$ SELECT CASE WHEN p_val IS NULL THEN 'null'::jsonb ELSE to_jsonb(p_val::TEXT) END; $function$ ; -- DROP FUNCTION v3._expense_item_path_set(_int4); CREATE OR REPLACE FUNCTION v3._expense_item_path_set(p_eids integer[]) RETURNS integer[] LANGUAGE sql STABLE AS $function$ WITH RECURSIVE up AS ( SELECT id, parent_id FROM v3.expense_item WHERE id = ANY(p_eids) UNION SELECT p.id, p.parent_id FROM v3.expense_item p JOIN up u ON p.id = u.parent_id ) SELECT array_agg(DISTINCT id) FROM up; $function$ ; -- DROP FUNCTION v3._jsonb_diff(jsonb, jsonb); CREATE OR REPLACE FUNCTION v3._jsonb_diff(a jsonb, b jsonb) RETURNS jsonb LANGUAGE plpgsql IMMUTABLE AS $function$ DECLARE result JSONB := '{}'::jsonb; k TEXT; va JSONB; vb JSONB; sub JSONB; BEGIN IF a IS NULL THEN RETURN COALESCE(b, '{}'::jsonb); END IF; IF b IS NULL THEN RETURN '{}'::jsonb; END IF; -- Ключи `b`: если отличаются от `a` — добавляем FOR k IN SELECT jsonb_object_keys(b) LOOP va := a -> k; vb := b -> k; IF jsonb_typeof(va) = 'object' AND jsonb_typeof(vb) = 'object' THEN sub := v3._jsonb_diff(va, vb); IF sub <> '{}'::jsonb THEN result := result || jsonb_build_object(k, sub); END IF; ELSIF va IS DISTINCT FROM vb THEN result := result || jsonb_build_object(k, vb); END IF; END LOOP; -- Ключи которые БЫЛИ в `a` но УДАЛЕНЫ в `b` — представляем как null FOR k IN SELECT jsonb_object_keys(a) LOOP IF NOT (b ? k) THEN result := result || jsonb_build_object(k, NULL); END IF; END LOOP; RETURN result; END; $function$ ; -- DROP FUNCTION v3._limit_template_path_set(_int4); CREATE OR REPLACE FUNCTION v3._limit_template_path_set(p_ids integer[]) RETURNS integer[] LANGUAGE sql STABLE AS $function$ WITH RECURSIVE up AS ( SELECT id, parent_id FROM v3.limit_template WHERE id = ANY(p_ids) UNION SELECT p.id, p.parent_id FROM v3.limit_template p JOIN up u ON p.id = u.parent_id ) SELECT array_agg(DISTINCT id) FROM up; $function$ ; -- DROP FUNCTION v3.add_budget_form(varchar, int4, int4, int4); CREATE OR REPLACE FUNCTION v3.add_budget_form(p_form_type character varying, p_year integer, p_org_unit_id integer DEFAULT NULL::integer, p_user_id integer DEFAULT NULL::integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_form_id INTEGER; v_ssp_list JSONB := '[]'::jsonb; BEGIN IF p_user_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id) THEN RAISE EXCEPTION 'app_user id=% не существует', p_user_id; END IF; IF p_org_unit_id IS NOT NULL AND 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; INSERT INTO v3.budget_form (form_type_code, year, org_unit_id, created_by) VALUES (p_form_type, p_year, p_org_unit_id, p_user_id) RETURNING id INTO v_form_id; PERFORM v3.copy_template_to_form(v_form_id); IF p_org_unit_id IS NOT NULL THEN SELECT jsonb_build_array( jsonb_build_object( 'id', ou.id, 'title', ou.title ) ) INTO v_ssp_list FROM v3.org_unit ou WHERE ou.id = p_org_unit_id; END IF; PERFORM v3.log_event( 'CREATE_TASK', 'TASK', jsonb_build_object( 'form_type', p_form_type, 'year', p_year, 'org_unit_id', p_org_unit_id, 'ssp_list', COALESCE(v_ssp_list, '[]'::jsonb), 'entity_id', v_form_id ), v_form_id, null, p_org_unit_id ); RETURN v_form_id; END; $function$ ; -- DROP FUNCTION v3.add_budget_line(int4, int4, varchar, varchar, varchar, varchar, varchar, varchar, int4, int4, varchar, varchar, date); CREATE OR REPLACE FUNCTION v3.add_budget_line(p_form_id integer, p_expense_item_id integer DEFAULT NULL::integer, p_sheet character varying DEFAULT NULL::character varying, p_item_id character varying DEFAULT NULL::character varying, p_section_code character varying DEFAULT NULL::character varying, p_direction character varying DEFAULT NULL::character varying, p_name character varying DEFAULT NULL::character varying, p_internal_order character varying DEFAULT NULL::character varying, p_vsp_id integer DEFAULT NULL::integer, p_project_id integer DEFAULT NULL::integer, p_justification character varying DEFAULT NULL::character varying, p_contract_number character varying DEFAULT NULL::character varying, p_contract_end_date date DEFAULT NULL::date) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_form_type VARCHAR; v_org_id INT; 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; BEGIN -- 1. form_id → form_type SELECT form_type_code, org_unit_id INTO v_form_type, v_org_id 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; -- ═══ Сателлитные листы (AHR_RENT/UTILITY/SECURITY) ═══════════════════ -- Фиксированный expense_item_id на лист — UI не передаёт R-код. -- p_vsp_id обязателен. Опц.: p_contract_number, p_contract_end_date. -- Внутри: находим/создаём родительскую budget_line с фиксированным ei, -- затем INSERT в *_detail. Возврат — одна новая LEAF. IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN v_sat_table := CASE p_sheet WHEN 'AHR_RENT' THEN 'rent_detail' WHEN 'AHR_UTILITY' THEN 'utility_detail' WHEN 'AHR_SECURITY' THEN 'security_detail' END; v_sat_eid := CASE p_sheet WHEN 'AHR_RENT' THEN 293 -- R061031001 "Аренда_ОН" WHEN 'AHR_UTILITY' THEN 24 -- 1.05.1. "Коммунальные услуги" WHEN 'AHR_SECURITY' THEN 779 -- R061062001 "Пультовая охр.ОН" END; IF p_vsp_id IS NULL THEN RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet; END IF; IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id; END IF; -- Находим/создаём родительскую budget_line под фиксированным ei 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; -- INSERT в сателлит EXECUTE format( 'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) ' || 'VALUES ($1, $2, $3, $4) RETURNING id', v_sat_table ) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date INTO v_new_id; -- Возврат — одна новая INPUT-строка (отфильтрована по новому 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; -- 2. Резолв expense_item_id IF p_expense_item_id IS NOT NULL THEN SELECT ei.id, ei.sheet INTO v_eid, v_sheet FROM v3.expense_item ei WHERE ei.id = p_expense_item_id; IF v_eid IS NULL THEN RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id; END IF; IF NOT EXISTS ( SELECT 1 FROM v3.expense_item_form_type WHERE expense_item_id = v_eid AND form_type_code = v_form_type ) THEN RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type; END IF; ELSE IF p_sheet IS NULL OR p_item_id IS NULL THEN RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)'; END IF; SELECT count(*) INTO v_cnt FROM v3.expense_item ei JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id WHERE ei.item_id = p_item_id AND ei.sheet = p_sheet AND eift.form_type_code = v_form_type AND (p_section_code IS NULL OR ei.section_code = p_section_code) AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction); IF v_cnt = 0 THEN RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%', p_sheet, p_item_id, p_section_code, p_direction, v_form_type; END IF; IF v_cnt > 1 THEN RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую', v_cnt, p_sheet, p_item_id, p_section_code, p_direction; END IF; SELECT ei.id, ei.sheet INTO v_eid, v_sheet FROM v3.expense_item ei JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id WHERE ei.item_id = p_item_id AND ei.sheet = p_sheet AND eift.form_type_code = v_form_type AND (p_section_code IS NULL OR ei.section_code = p_section_code) AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction); END IF; -- 3. Авто-direction (для FORM_1/AHR|CAP — обязателен у юзера) 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); -- 4. Sanity на vsp/project 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.project WHERE id = p_project_id) THEN RAISE EXCEPTION 'project id=% не существует', p_project_id; END IF; -- 5. INSERT INSERT INTO v3.budget_line ( budget_form_id, expense_item_id, name, internal_order, vsp_id, project_id, direction, justification ) VALUES ( p_form_id, v_eid, p_name, p_internal_order, p_vsp_id, p_project_id, v_final_dir, p_justification ) RETURNING id INTO v_new_id; -- 5а. Аудит: ROW_CREATE PERFORM v3.log_event( 'ROW_CREATE', 'ROW', jsonb_build_object( 'sheet', v_sheet, 'line_id', v_new_id, 'expense_item_id', v_eid, 'direction', v_final_dir, 'name', p_name, 'vsp_id', p_vsp_id, 'project_id', p_project_id, 'entity_id', v_new_id, 'form_id', p_form_id ), p_form_id, null, v_org_id ); -- 6. Возврат: только иерархия по пути нового ei + новая INPUT DECLARE v_ei_path INT[]; BEGIN v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]); RETURN QUERY SELECT v.row_type, v.depth, v.sort_order, v.data FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM') AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id) ORDER BY v.sort_order; END; END; $function$ ; -- DROP FUNCTION v3.add_form3_line(int4, int4); CREATE OR REPLACE FUNCTION v3.add_form3_line(p_report_id integer, p_expense_item_id integer) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_new_id INT; v_project_id INT; v_org_id INT; BEGIN SELECT project_id INTO v_project_id FROM v3.rf_project_report WHERE id = p_report_id; IF v_project_id IS NULL THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; IF NOT EXISTS (SELECT 1 FROM v3.expense_item WHERE id = p_expense_item_id) THEN RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id; END IF; IF NOT EXISTS (SELECT 1 FROM v3.expense_item_form_type WHERE expense_item_id = p_expense_item_id AND form_type_code = 'FORM_3') THEN RAISE EXCEPTION 'expense_item id=% не привязан к form_type FORM_3', p_expense_item_id; END IF; INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id) VALUES (p_report_id, p_expense_item_id) RETURNING id INTO v_new_id; SELECT org_unit_id INTO v_org_id FROM v3.project WHERE id = v_project_id; -- Аудит: ROW_CREATE PERFORM v3.log_event( 'ROW_CREATE', 'ROW', jsonb_build_object( 'sheet', 'FORM_3', 'line_id', v_new_id, 'report_id', p_report_id, 'expense_item_id', p_expense_item_id, 'entity_id', v_new_id ), null, null, v_org_id ); -- Возврат: только иерархия по пути нового ei + новая INPUT DECLARE v_ei_path INT[]; BEGIN v_ei_path := v3._expense_item_path_set(ARRAY[p_expense_item_id]); RETURN QUERY SELECT v.row_type, v.depth, v.sort_order, v.data FROM v3.v_form3_report_jsonb(p_report_id, NULL) v WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM') AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id) ORDER BY v.sort_order; END; END; $function$ ; -- DROP FUNCTION v3.add_form_phase(int4, varchar, varchar, varchar, _text, timestamptz, timestamptz); CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp with time zone, p_closes_at timestamp with time zone) RETURNS v3.form_phase LANGUAGE plpgsql AS $function$ DECLARE v_phase v3.form_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; BEGIN INSERT INTO v3.form_phase( budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at ) VALUES ( p_budget_form_id, p_sheet, p_phase_code, p_role, p_column_keys, p_opens_at, p_closes_at ) RETURNING * INTO v_phase; SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year FROM v3.budget_form bf LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id WHERE bf.id = p_budget_form_id; v_task_name := trim(concat( COALESCE(v_form_type_code, ''), CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'form_phase', 'entity_id', p_budget_form_id, 'action', 'create', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'role', v_phase.role, 'column_keys', v_phase.column_keys, 'opens_at', v_phase.opens_at, 'closes_at', v_phase.closes_at ), p_budget_form_id, null, v_org_unit_id ); RETURN v_phase; END; $function$ ; -- DROP FUNCTION v3.add_project(varchar, int4, int4, varchar, int4, varchar, varchar, varchar, varchar, int4, numeric); 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) 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; 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 WHERE id = p_org_unit_id) 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; -- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type -- из migrate_form3_v1.sql применятся автоматически). INSERT INTO v3.project ( name, level, parent_id, org_unit_id, project_type, vsp_format, placement_type, object_address, staff_count, total_area ) 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 ) RETURNING id INTO v_pid; -- Создаём 2 отчёта (LIMIT + CURRENT_EXPENSES) для проекта × год INSERT INTO v3.rf_project_report (project_id, year, report_type) VALUES (v_pid, p_year, 'LIMIT') RETURNING id INTO v_lim; INSERT INTO v3.rf_project_report (project_id, year, report_type) VALUES (v_pid, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur; -- Аудит: 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, 'staff_count', p_staff_count, 'total_area', p_total_area, 'limit_report_id', v_lim, 'current_expenses_report_id', v_cur, 'entity_id', v_pid )), null, null, p_org_unit_id ); RETURN QUERY SELECT v_pid, v_lim, v_cur; END; $function$ ; -- DROP FUNCTION v3.add_user(varchar, varchar, varchar, varchar, int4); CREATE OR REPLACE FUNCTION v3.add_user(p_email character varying, p_username character varying, p_hashed_password character varying, p_full_name character varying, p_role_id integer) RETURNS v3.app_user LANGUAGE plpgsql AS $function$ DECLARE v_user v3.app_user; BEGIN INSERT INTO v3.app_user(email, username, hashed_password, full_name, role_id, is_active) VALUES (p_email, p_username, p_hashed_password, p_full_name, p_role_id, TRUE) RETURNING * INTO v_user; PERFORM v3.log_event( 'USER_CREATE', 'USER', jsonb_strip_nulls(jsonb_build_object( 'user_id', v_user.id, 'email', v_user.email, 'username', v_user.username, 'full_name', v_user.full_name, 'role_id', v_user.role_id, 'entity_id', v_user.id )) ); RETURN v_user; END; $function$ ; -- DROP FUNCTION v3.add_vsp(int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, date); CREATE OR REPLACE FUNCTION v3.add_vsp(p_branch_id integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT true, p_is_deleted boolean DEFAULT false, p_system_code character varying DEFAULT NULL::character varying, p_created_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_id INT; v_branch_name VARCHAR; v_actor_id INT; v_actor_email VARCHAR; v_actor_full_name VARCHAR; BEGIN SELECT ou.title INTO v_branch_name FROM v3.org_unit ou WHERE ou.id = p_branch_id; v_actor_id := COALESCE(p_created_by, v3.current_user_id()); IF v_actor_id IS NOT NULL THEN SELECT u.email, u.full_name INTO v_actor_email, v_actor_full_name FROM v3.app_user u WHERE u.id = v_actor_id; END IF; INSERT INTO v3.vsp (branch_id, reg_number, address, format, opened_at, placement_type, staff_count, total_area, closed_at, is_active, is_deleted, system_code, created_by, vsp_type, notes, rent_contract_num, rent_end_date) VALUES (p_branch_id, p_reg_number, p_address, p_format, p_opened_at, p_placement_type, p_staff_count, p_total_area, p_closed_at, p_is_active, p_is_deleted, p_system_code, p_created_by, p_vsp_type, p_notes, p_rent_contract_num, p_rent_end_date) RETURNING id INTO v_id; PERFORM v3.log_event( 'VSP_CREATE', 'VSP', jsonb_build_object( 'vsp_id', v_id, 'entity_id', v_id, 'branch_id', p_branch_id, 'branch_name', v_branch_name, 'reg_number', p_reg_number, 'address', p_address, 'format', p_format, 'opened_at', v3._diff_val(p_opened_at), 'placement_type', p_placement_type, 'staff_count', p_staff_count, 'total_area', v3._diff_val(p_total_area), 'closed_at', v3._diff_val(p_closed_at), 'is_active', p_is_active, 'is_deleted', p_is_deleted, 'system_code', p_system_code, 'created_by', p_created_by, 'vsp_type', p_vsp_type, 'notes', p_notes, 'rent_contract_num', p_rent_contract_num, 'rent_end_date', v3._diff_val(p_rent_end_date), 'user_email', v_actor_email, 'user_full_name', v_actor_full_name ), null, null, p_branch_id ); RETURN v_id; END; $function$ ; -- DROP FUNCTION v3.can_edit(int4, varchar, text, int4); CREATE OR REPLACE FUNCTION v3.can_edit(p_form_id integer, p_sheet character varying, p_column_key text, p_user_id integer) RETURNS jsonb LANGUAGE plpgsql STABLE AS $function$ DECLARE v_role VARCHAR; v_active_close TIMESTAMPTZ; v_role_has_col BOOLEAN; v_next_open TIMESTAMPTZ; v_last_close TIMESTAMPTZ; BEGIN IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN RAISE EXCEPTION 'budget_form id=% не существует', p_form_id; END IF; v_role := v3.user_role_code(p_user_id); IF v_role IS NULL THEN RETURN jsonb_build_object( 'ok', false, 'code', 'role_not_allowed', 'detail', 'unknown or inactive user' ); END IF; -- ADMIN — bypass всех проверок. IF v_role = 'ADMIN' THEN RETURN jsonb_build_object('ok', true, 'admin', true); END IF; -- EXECUTOR_RF: фильтр по user_org формы. IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_form_org(p_user_id, p_form_id) THEN RETURN jsonb_build_object( 'ok', false, 'code', 'org_not_assigned', 'detail', p_column_key ); END IF; -- 1. Активная сейчас фаза, в которой эта колонка перечислена для этой роли? SELECT MAX(fp.closes_at) INTO v_active_close FROM v3.form_phase fp WHERE fp.budget_form_id = p_form_id AND fp.sheet = p_sheet AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) AND now() BETWEEN fp.opens_at AND fp.closes_at; IF v_active_close IS NOT NULL THEN RETURN jsonb_build_object( 'ok', true, 'closes_at', v_active_close ); END IF; -- 2. Колонка В ПРИНЦИПЕ есть хоть в одной фазе этой роли (на этой форме/листе)? -- Если нет — это вопрос whitelist, отдаём role_not_allowed. SELECT EXISTS ( SELECT 1 FROM v3.form_phase fp WHERE fp.budget_form_id = p_form_id AND fp.sheet = p_sheet AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) ) INTO v_role_has_col; IF NOT v_role_has_col THEN RETURN jsonb_build_object( 'ok', false, 'code', 'role_not_allowed', 'detail', p_column_key ); END IF; -- 3. Колонка доступна, но окно не активно. Подсказать ближайшее. SELECT MIN(fp.opens_at) INTO v_next_open FROM v3.form_phase fp WHERE fp.budget_form_id = p_form_id AND fp.sheet = p_sheet AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) AND fp.opens_at > now(); IF v_next_open IS NOT NULL THEN RETURN jsonb_build_object( 'ok', false, 'code', 'window_closed', 'detail', p_column_key, 'opens_at', v_next_open ); END IF; SELECT MAX(fp.closes_at) INTO v_last_close FROM v3.form_phase fp WHERE fp.budget_form_id = p_form_id AND fp.sheet = p_sheet AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) AND fp.closes_at <= now(); RETURN jsonb_build_object( 'ok', false, 'code', 'window_closed', 'detail', p_column_key, 'closes_at', v_last_close ); END; $function$ ; -- DROP FUNCTION v3.copy_template_to_form(int4); CREATE OR REPLACE FUNCTION v3.copy_template_to_form(p_form_id integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_form_type VARCHAR; v_inserted INTEGER; BEGIN SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id; IF NOT FOUND THEN RAISE EXCEPTION 'budget_form id=% не существует', p_form_id; END IF; INSERT INTO v3.form_phase (budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at) SELECT p_form_id, pt.sheet, pt.phase_code, pt.role, pt.column_keys, pt.opens_at, pt.closes_at FROM v3.phase_template pt WHERE pt.form_type = v_form_type ON CONFLICT (budget_form_id, sheet, phase_code) DO NOTHING; GET DIAGNOSTICS v_inserted = ROW_COUNT; RETURN v_inserted; END; $function$ ; -- DROP FUNCTION v3.current_user_id(); CREATE OR REPLACE FUNCTION v3.current_user_id() RETURNS integer LANGUAGE plpgsql STABLE AS $function$ DECLARE v_raw TEXT; v_id INTEGER; BEGIN v_raw := current_setting('app.user_id', true); IF v_raw IS NULL OR v_raw = '' THEN RETURN NULL; END IF; BEGIN v_id := v_raw::INTEGER; EXCEPTION WHEN OTHERS THEN RETURN NULL; END; RETURN v_id; END; $function$ ; -- DROP FUNCTION v3.deactivate_vsp(int4); CREATE OR REPLACE FUNCTION v3.deactivate_vsp(p_vsp_id integer) RETURNS date LANGUAGE plpgsql AS $function$ DECLARE v_closed DATE; v_old_closed DATE; v_branch_id INT; v_branch_name VARCHAR; v_actor_id INT; v_actor_email VARCHAR; v_actor_full_name VARCHAR; BEGIN SELECT v.branch_id, v.closed_at, ou.title INTO v_branch_id, v_old_closed, v_branch_name FROM v3.vsp v LEFT JOIN v3.org_unit ou ON ou.id = v.branch_id WHERE v.id = p_vsp_id; IF NOT FOUND THEN RAISE EXCEPTION 'vsp #% не существует', p_vsp_id; END IF; IF v_old_closed IS NOT NULL THEN RAISE EXCEPTION 'vsp #% уже закрыт', p_vsp_id; END IF; UPDATE v3.vsp SET closed_at = CURRENT_DATE WHERE id = p_vsp_id RETURNING closed_at INTO v_closed; v_actor_id := v3.current_user_id(); IF v_actor_id IS NOT NULL THEN SELECT u.email, u.full_name INTO v_actor_email, v_actor_full_name FROM v3.app_user u WHERE u.id = v_actor_id; END IF; PERFORM v3.log_event( 'VSP_UPDATE', 'VSP', jsonb_build_object( 'vsp_id', p_vsp_id, 'branch_id', v_branch_id, 'branch_name', v_branch_name, 'changes', jsonb_build_object( 'closed_at', jsonb_build_object( 'before', v3._diff_val(v_old_closed), 'after', v3._diff_val(v_closed) ) ), 'closed_at', v_closed::TEXT, 'user_id', v_actor_id, 'user_email', v_actor_email, 'user_full_name', v_actor_full_name ), null, null, v_branch_id); RETURN v_closed; END; $function$ ; -- DROP FUNCTION v3.del_budget_form(int4); CREATE OR REPLACE FUNCTION v3.del_budget_form(p_form_id integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_old RECORD; BEGIN SELECT bf.form_type_code, bf.year, bf.org_unit_id INTO v_old FROM v3.budget_form bf WHERE bf.id = p_form_id; IF NOT FOUND THEN RAISE EXCEPTION 'budget_form #% не существует', p_form_id; END IF; DELETE FROM v3.budget_form WHERE id = p_form_id; PERFORM v3.log_event( 'DELETE_TASK', 'TASK', jsonb_build_object( 'form_id', p_form_id, 'form_type', v_old.form_type_code, 'year', v_old.year, 'org_unit_id', v_old.org_unit_id), p_form_id, null, v_old.org_unit_id); RETURN p_form_id; END; $function$ ; -- DROP FUNCTION v3.del_budget_line(int4, varchar, varchar); CREATE OR REPLACE FUNCTION v3.del_budget_line(p_id integer, p_direction character varying DEFAULT NULL::character varying, p_sheet character varying DEFAULT NULL::character varying) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_form_id INT; v_org_id INT; v_sheet VARCHAR; v_sat_table TEXT; v_eid INT; v_ei_path INT[]; BEGIN -- ═══ Сателлитные листы — DELETE из *_detail по id ═════════════════════ 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; -- Извлекаем form_id для возврата (через JOIN на budget_line) EXECUTE format( 'SELECT bl.budget_form_id FROM v3.%I s JOIN v3.budget_line bl ON bl.id = s.line_id WHERE s.id = $1', v_sat_table ) USING p_id INTO v_form_id; IF v_form_id IS NULL THEN RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_id; END IF; EXECUTE format('DELETE FROM v3.%I WHERE id = $1', v_sat_table) USING p_id; -- Возврат пустой — UI сам уберёт строку по id RETURN; END IF; -- ═══ Основная сетка ═══════════════════════════════════════════════════ -- Sanity + извлечение form_id/sheet/expense_item_id (нужен для path -- ДО DELETE — после удаления budget_line его уже не получить). SELECT bl.budget_form_id, ei.sheet, bl.expense_item_id INTO v_form_id, v_sheet, v_eid FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id WHERE bl.id = p_id; IF v_form_id IS NULL THEN RAISE EXCEPTION 'budget_line id=% не существует', p_id; END IF; v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]); -- Каскадное удаление дочерних записей DELETE FROM v3.plan WHERE line_id = p_id; DELETE FROM v3.sequestration WHERE line_id = p_id; DELETE FROM v3.reserve WHERE line_id = p_id; DELETE FROM v3.allocation WHERE line_id = p_id; DELETE FROM v3.contract_summary WHERE line_id = p_id; DELETE FROM v3.collegial_approval WHERE line_id = p_id; DELETE FROM v3.ckk WHERE line_id = p_id; DELETE FROM v3.contract_detail WHERE line_id = p_id; DELETE FROM v3.booking WHERE line_id = p_id; DELETE FROM v3.budget_line_quarter WHERE line_id = p_id; DELETE FROM v3.rent_detail WHERE line_id = p_id; DELETE FROM v3.utility_detail WHERE line_id = p_id; DELETE FROM v3.security_detail WHERE line_id = p_id; -- AHR_LIMIT с v3 не имеет привязки к budget_line — каталог limit_template -- глобальный, per-form qty в form_limit удаляется через ON DELETE CASCADE -- от budget_form. -- Удаляем сам budget_line DELETE FROM v3.budget_line WHERE id = p_id; SELECT org_unit_id INTO v_org_id FROM v3.budget_form WHERE id = v_form_id; -- Аудит: ROW_DELETE PERFORM v3.log_event( 'ROW_DELETE', 'ROW', jsonb_build_object( 'sheet', v_sheet, 'line_id', p_id, 'entity_id', p_id, 'expense_item_id', v_eid, 'direction', p_direction ), v_form_id, NULL, v_org_id ); -- Возврат: иерархия только по пути удалённой строки (INPUT уже нет) RETURN QUERY SELECT v.row_type, v.depth, v.sort_order, v.data FROM v3.v_form_view(v_form_id, v_sheet, NULL, p_direction) v WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM') AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path) ORDER BY v.sort_order; END; $function$ ; -- DROP FUNCTION v3.del_form3_line(int4); CREATE OR REPLACE FUNCTION v3.del_form3_line(p_line_id integer) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_report_id INT; v_eid INT; v_form_id INT; v_ei_path INT[]; BEGIN -- Извлекаем report_id + ei ДО DELETE (нужен для path) SELECT rfprl.rf_project_report_id, rfprl.expense_item_id INTO v_report_id, v_eid FROM v3.rf_project_report_line rfprl WHERE rfprl.id = p_line_id; IF v_report_id IS NULL THEN RAISE EXCEPTION 'rf_project_report_line id=% не существует', p_line_id; END IF; v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]); SELECT bf.id INTO v_form_id FROM v3.rf_project_report r JOIN v3.project p ON p.id = r.project_id LEFT JOIN v3.budget_form bf ON bf.form_type_code = 'FORM_3' AND bf.year = r.year AND bf.org_unit_id = p.org_unit_id WHERE r.id = v_report_id LIMIT 1; -- Каскад (FK без ON DELETE CASCADE) DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id = p_line_id; DELETE FROM v3.rf_project_report_line WHERE id = p_line_id; -- Аудит: ROW_DELETE PERFORM v3.log_event( 'ROW_DELETE', 'ROW', jsonb_build_object( 'sheet', 'FORM_3', 'line_id', p_line_id, 'form_id', v_form_id, 'report_id', v_report_id, 'expense_item_id', v_eid ), v_form_id ); -- Возврат: только иерархия по пути удалённой (INPUT уже нет) RETURN QUERY SELECT v.row_type, v.depth, v.sort_order, v.data FROM v3.v_form3_report_jsonb(v_report_id, NULL) v WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM') AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path) ORDER BY v.sort_order; END; $function$ ; -- DROP FUNCTION v3.del_form_phase(int4, varchar, varchar); CREATE OR REPLACE FUNCTION v3.del_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying) RETURNS boolean LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; BEGIN DELETE FROM v3.form_phase WHERE budget_form_id = p_budget_form_id AND sheet = p_sheet AND phase_code = p_phase_code RETURNING * INTO v_old; IF NOT FOUND THEN RETURN FALSE; END IF; SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year FROM v3.budget_form bf LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id WHERE bf.id = p_budget_form_id; v_task_name := trim(concat( COALESCE(v_form_type_code, ''), CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'form_phase', 'entity_id', p_budget_form_id, 'action', 'delete', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'task_name', NULLIF(v_task_name, ''), 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'role', v_old.role, 'column_keys', v_old.column_keys, 'opens_at', v_old.opens_at, 'closes_at', v_old.closes_at ), p_budget_form_id, null, v_org_unit_id ); RETURN TRUE; END; $function$ ; -- DROP FUNCTION v3.del_project(int4); CREATE OR REPLACE FUNCTION v3.del_project(p_project_id integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_old RECORD; BEGIN SELECT p.name, p.level, p.org_unit_id INTO v_old FROM v3.project p WHERE p.id = p_project_id; IF NOT FOUND THEN RAISE EXCEPTION 'project #% не существует', p_project_id; END IF; -- Каскад вручную: rf_project_report_quarter / line / report (нет ON DELETE) DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id IN ( SELECT l.id FROM v3.rf_project_report_line l JOIN v3.rf_project_report r ON r.id = l.rf_project_report_id WHERE r.project_id = p_project_id); DELETE FROM v3.rf_project_report_line WHERE rf_project_report_id IN ( SELECT id FROM v3.rf_project_report WHERE project_id = p_project_id); DELETE FROM v3.rf_project_report WHERE project_id = p_project_id; DELETE FROM v3.project WHERE id = p_project_id; PERFORM v3.log_event( 'PROJECT_DELETE', 'PROJECT', jsonb_build_object( 'project_id', p_project_id, 'name', v_old.name, 'level', v_old.level, 'org_unit_id', v_old.org_unit_id), null, null, v_old.org_unit_id); RETURN p_project_id; END; $function$ ; -- DROP FUNCTION v3.del_vsp(int4, int4); CREATE OR REPLACE FUNCTION v3.del_vsp(p_vsp_id integer, p_deleted_by integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_old RECORD; v_usr RECORD; v_closed DATE; BEGIN SELECT vsp.id, vsp.branch_id INTO v_old FROM v3.vsp vsp WHERE vsp.id = p_vsp_id; IF NOT FOUND THEN RAISE EXCEPTION 'vsp #% не существует', p_vsp_id; END IF; SELECT usr.id INTO v_usr FROM v3.app_user usr WHERE usr.id = p_deleted_by; IF NOT FOUND THEN RAISE EXCEPTION 'user #% не существует', p_deleted_by; END IF; UPDATE v3.vsp SET is_deleted = true, updated_by = p_deleted_by WHERE id = p_vsp_id; UPDATE v3.vsp SET closed_at = CURRENT_DATE WHERE id = p_vsp_id AND closed_at IS NULL RETURNING closed_at INTO v_closed; IF v_closed IS NULL THEN IF EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN RAISE EXCEPTION 'vsp #% уже закрыт', p_vsp_id; ELSE RAISE EXCEPTION 'vsp #% не существует', p_vsp_id; END IF; END IF; PERFORM v3.log_event( 'DELETE_VSP', 'VSP', jsonb_build_object( 'vsp_id', p_vsp_id, 'entity_id', p_vsp_id, 'branch_id', v_old.branch_id, 'updated_by', p_deleted_by ), --vsp_id, null, null, v_old.branch_id); RETURN p_vsp_id; END; $function$ ; -- DROP FUNCTION v3.editable_columns_for(int4, int4, varchar); CREATE OR REPLACE FUNCTION v3.editable_columns_for(p_form_id integer, p_user_id integer, p_sheet character varying) RETURNS TABLE(column_key text, closes_at timestamp with time zone) LANGUAGE plpgsql STABLE AS $function$ DECLARE v_role VARCHAR; BEGIN IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN RAISE EXCEPTION 'budget_form id=% не существует', p_form_id; END IF; v_role := v3.user_role_code(p_user_id); IF v_role IS NULL THEN RETURN; -- неизвестный/неактивный юзер → пустая маска END IF; -- ADMIN: вернуть UNION всех column_keys всех phase'ов на форме/листе. IF v_role = 'ADMIN' THEN RETURN QUERY SELECT ck AS column_key, MAX(fp.closes_at) AS closes_at FROM v3.form_phase fp, LATERAL unnest(fp.column_keys) AS ck WHERE fp.budget_form_id = p_form_id AND fp.sheet = p_sheet GROUP BY ck; RETURN; END IF; -- EXECUTOR_RF без назначения → пустая маска (default deny). IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_form_org(p_user_id, p_form_id) THEN RETURN; END IF; RETURN QUERY SELECT ck AS column_key, MAX(fp.closes_at) AS closes_at FROM v3.form_phase fp, LATERAL unnest(fp.column_keys) AS ck WHERE fp.budget_form_id = p_form_id AND fp.sheet = p_sheet AND fp.role = v_role AND now() BETWEEN fp.opens_at AND fp.closes_at GROUP BY ck; END; $function$ ; -- DROP FUNCTION v3.grant_user_org_access(int4, int4); CREATE OR REPLACE FUNCTION v3.grant_user_org_access(p_user_id integer, p_org_unit_id integer) RETURNS boolean LANGUAGE plpgsql AS $function$ DECLARE v_inserted INT; v_user_email VARCHAR; v_user_full_name VARCHAR; v_org_name VARCHAR; BEGIN SELECT email, full_name INTO v_user_email, v_user_full_name FROM v3.app_user WHERE id = p_user_id; IF NOT FOUND THEN RAISE EXCEPTION 'app_user #% не существует', p_user_id; END IF; SELECT title INTO v_org_name FROM v3.org_unit WHERE id = p_org_unit_id; IF v_org_name IS NULL THEN RAISE EXCEPTION 'org_unit #% не существует', p_org_unit_id; END IF; INSERT INTO v3.user_org(user_id, org_unit_id) VALUES (p_user_id, p_org_unit_id) ON CONFLICT (user_id, org_unit_id) DO NOTHING; GET DIAGNOSTICS v_inserted = ROW_COUNT; IF v_inserted > 0 THEN PERFORM v3.log_event( 'USER_ACCESS_GRANTED', 'USER', jsonb_build_object( 'user_id', p_user_id, 'entity_id', p_user_id, 'email', v_user_email, 'full_name', v_user_full_name, 'org_unit_id', p_org_unit_id, 'org_unit_name', v_org_name ), null, null, p_org_unit_id ); RETURN TRUE; END IF; RETURN FALSE; END; $function$ ; -- DROP FUNCTION v3.jsonb_numeric_sum(jsonb, jsonb); CREATE OR REPLACE FUNCTION v3.jsonb_numeric_sum(a jsonb, b jsonb) RETURNS jsonb LANGUAGE sql IMMUTABLE AS $function$ SELECT CASE WHEN a IS NULL THEN b WHEN b IS NULL THEN a WHEN jsonb_typeof(a) = 'number' AND jsonb_typeof(b) = 'number' THEN to_jsonb((a)::text::numeric + (b)::text::numeric) WHEN jsonb_typeof(a) = 'object' AND jsonb_typeof(b) = 'object' THEN ( SELECT jsonb_object_agg(k, v3.jsonb_numeric_sum(a -> k, b -> k) ) FROM ( SELECT k FROM jsonb_object_keys(a) k UNION SELECT k FROM jsonb_object_keys(b) k ) keys ) -- одна сторона число/объект, другая нет → берём числовую/объектную WHEN jsonb_typeof(a) IN ('number','object') THEN a WHEN jsonb_typeof(b) IN ('number','object') THEN b ELSE NULL END; $function$ ; -- DROP AGGREGATE v3.jsonb_sum(jsonb); CREATE OR REPLACE AGGREGATE v3.jsonb_sum(pg_catalog.jsonb) ( SFUNC = v3.jsonb_numeric_sum, STYPE = jsonb ); -- DROP FUNCTION v3.log_event(text, text, jsonb, int4, int4, int4); CREATE OR REPLACE FUNCTION v3.log_event(p_event text, p_event_type text, p_data jsonb, p_form_id integer DEFAULT NULL::integer, p_task_id integer DEFAULT NULL::integer, p_org_unit_id integer DEFAULT NULL::integer) RETURNS bigint LANGUAGE plpgsql AS $function$ DECLARE v_user_id INTEGER; v_audit_id BIGINT; BEGIN v_user_id := v3.current_user_id(); -- Невалидный user_id → пишем NULL (не падаем). IF v_user_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = v_user_id) THEN v_user_id := NULL; END IF; INSERT INTO v3.audit_log (user_id, org_unit_id, form_id, task_id, event, event_type, event_data) VALUES (v_user_id, p_org_unit_id, p_form_id, p_task_id, p_event, p_event_type, COALESCE(p_data, '{}'::jsonb)) RETURNING id INTO v_audit_id; RETURN v_audit_id; END; $function$ ; -- DROP FUNCTION v3.revoke_many_user_org_access(int4, _int4); CREATE OR REPLACE FUNCTION v3.revoke_many_user_org_access(p_user_id integer, p_org_unit_ids integer[]) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_org_id INT; v_count INT := 0; BEGIN IF p_org_unit_ids IS NULL OR cardinality(p_org_unit_ids) = 0 THEN RETURN 0; END IF; FOREACH v_org_id IN ARRAY p_org_unit_ids LOOP IF v3.revoke_user_org_access(p_user_id, v_org_id) THEN v_count := v_count + 1; END IF; END LOOP; RETURN v_count; END; $function$ ; -- DROP FUNCTION v3.revoke_user_org_access(int4, int4); CREATE OR REPLACE FUNCTION v3.revoke_user_org_access(p_user_id integer, p_org_unit_id integer) RETURNS boolean LANGUAGE plpgsql AS $function$ DECLARE v_deleted INT; v_user_email VARCHAR; v_user_full_name VARCHAR; v_org_name VARCHAR; BEGIN SELECT email, full_name INTO v_user_email, v_user_full_name FROM v3.app_user WHERE id = p_user_id; IF NOT FOUND THEN RAISE EXCEPTION 'app_user #% не существует', p_user_id; END IF; SELECT title INTO v_org_name FROM v3.org_unit WHERE id = p_org_unit_id; IF v_org_name IS NULL THEN RAISE EXCEPTION 'org_unit #% не существует', p_org_unit_id; END IF; DELETE FROM v3.user_org WHERE user_id = p_user_id AND org_unit_id = p_org_unit_id; GET DIAGNOSTICS v_deleted = ROW_COUNT; IF v_deleted > 0 THEN PERFORM v3.log_event( 'USER_ACCESS_REVOKED', 'USER', jsonb_build_object( 'user_id', p_user_id, 'entity_id', p_user_id, 'email', v_user_email, 'full_name', v_user_full_name, 'org_unit_id', p_org_unit_id, 'org_unit_name', v_org_name ), null, null, p_org_unit_id ); RETURN TRUE; END IF; RETURN FALSE; END; $function$ ; -- DROP FUNCTION v3.upd_budget_form(int4, int4, int4); CREATE OR REPLACE FUNCTION v3.upd_budget_form(p_form_id integer, p_year integer DEFAULT NULL::integer, p_org_unit_id integer DEFAULT NULL::integer) RETURNS TABLE(id integer, form_type_code character varying, year integer, org_unit_id integer) LANGUAGE plpgsql AS $function$ DECLARE v_old RECORD; v_changes JSONB := '{}'::jsonb; BEGIN SELECT bf.year, bf.org_unit_id INTO v_old FROM v3.budget_form bf WHERE bf.id = p_form_id; IF NOT FOUND THEN RAISE EXCEPTION 'budget_form #% не существует', p_form_id; END IF; IF p_year IS NOT NULL AND p_year IS DISTINCT FROM v_old.year THEN UPDATE v3.budget_form bf SET year = p_year WHERE bf.id = p_form_id; v_changes := v_changes || jsonb_build_object( 'year', jsonb_build_object( 'before', v3._diff_val(v_old.year), 'after', v3._diff_val(p_year))); END IF; IF p_org_unit_id IS NOT NULL AND p_org_unit_id IS DISTINCT FROM v_old.org_unit_id THEN UPDATE v3.budget_form bf SET org_unit_id = p_org_unit_id WHERE bf.id = p_form_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 v_changes <> '{}'::jsonb THEN UPDATE v3.budget_form bf SET updated_at = now() WHERE bf.id = p_form_id; PERFORM v3.log_event( 'UPDATE_TASK', 'UPDATE_TASK', jsonb_build_object('form_id', p_form_id, 'changes', v_changes), p_form_id, null, COALESCE(p_org_unit_id, v_old.org_unit_id)); END IF; RETURN QUERY SELECT bf.id, bf.form_type_code, bf.year, bf.org_unit_id FROM v3.budget_form bf WHERE bf.id = p_form_id; END; $function$ ; -- DROP FUNCTION v3.upd_form3_cells(int4, jsonb); CREATE OR REPLACE FUNCTION v3.upd_form3_cells(p_report_id integer, p_changes jsonb) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_change JSONB; v_line_id INT; v_column TEXT; v_value JSONB; v_changed_lines INT[] := ARRAY[]::INT[]; BEGIN IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN RAISE EXCEPTION 'p_changes must be a JSONB array'; END IF; IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes) LOOP v_line_id := (v_change->>'line_id')::INT; v_column := v_change->>'column'; v_value := v_change->'value'; IF v_line_id IS NULL OR v_column IS NULL THEN RAISE EXCEPTION 'change must have line_id and column: %', v_change; END IF; IF NOT EXISTS ( SELECT 1 FROM v3.rf_project_report_line WHERE id = v_line_id AND rf_project_report_id = p_report_id ) THEN RAISE EXCEPTION 'rf_project_report_line id=% не принадлежит report id=%', v_line_id, p_report_id; END IF; PERFORM v3._apply_form3_cell(v_line_id, v_column, v_value); IF NOT (v_line_id = ANY(v_changed_lines)) THEN v_changed_lines := v_changed_lines || v_line_id; END IF; END LOOP; -- Возврат: иерархия по пути изменённых rfprl + изменённые INPUT по line_id DECLARE v_eid_set INT[]; v_ei_path INT[]; BEGIN SELECT array_agg(DISTINCT rfprl.expense_item_id) INTO v_eid_set FROM v3.rf_project_report_line rfprl WHERE rfprl.id = ANY(v_changed_lines); v_ei_path := v3._expense_item_path_set(v_eid_set); RETURN QUERY SELECT v.row_type, v.depth, v.sort_order, v.data FROM v3.v_form3_report_jsonb(p_report_id, NULL) v WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM') AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(v_changed_lines)) ORDER BY v.sort_order; END; END; $function$ ; -- DROP FUNCTION v3.upd_form3_cell(int4, int4, text, jsonb); CREATE OR REPLACE FUNCTION v3.upd_form3_cell(p_report_id integer, p_line_id integer, p_column text, p_value jsonb) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql AS $function$ SELECT * FROM v3.upd_form3_cells( p_report_id, jsonb_build_array(jsonb_build_object( 'line_id', p_line_id, 'column', p_column, 'value', p_value )) ); $function$ ; -- DROP FUNCTION v3.upd_form_cells(int4, varchar, jsonb, varchar, _text); CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_change JSONB; v_line_id INT; v_column TEXT; v_value JSONB; v_changed_lines INT[] := ARRAY[]::INT[]; v_before JSONB := '{}'::jsonb; -- map sort_order(text) → data BEFORE v_lc_path INT[]; v_eid_set INT[]; v_ei_path INT[]; BEGIN IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN RAISE EXCEPTION 'p_changes must be a JSONB array'; END IF; -- Проверка: form_id существует IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN RAISE EXCEPTION 'budget_form id=% не существует', p_form_id; END IF; -- Применяем все изменения в одной транзакции (вся функция = одна tx) FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes) LOOP v_line_id := (v_change->>'line_id')::INT; v_column := v_change->>'column'; v_value := v_change->'value'; -- именно ->, чтобы NULL остался jsonb null IF v_line_id IS NULL OR v_column IS NULL THEN RAISE EXCEPTION 'change must have line_id and column: %', v_change; END IF; -- Sanity: для основной сетки — line_id принадлежит budget_form; -- для сателлитов — id принадлежит соответствующей таблице сателлита -- и через её line_id — данной budget_form; -- для AHR_LIMIT — id принадлежит limit_template (глобальный каталог, -- проверим что строка LEAF; per-form значения летят в form_limit). IF p_sheet = 'AHR_LIMIT' THEN IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt WHERE lt.id = v_line_id AND lt.row_type = 'LEAF') THEN RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF', v_line_id; END IF; ELSIF p_sheet = 'OTCH9F' THEN IF NOT EXISTS (SELECT 1 FROM v3.fixed_asset_report WHERE id = v_line_id AND budget_form_id = p_form_id) THEN RAISE EXCEPTION 'fixed_asset_report.id=% не принадлежит budget_form id=%', v_line_id, p_form_id; END IF; ELSIF p_sheet = 'AHR_RENT' THEN IF NOT EXISTS (SELECT 1 FROM v3.rent_detail rd JOIN v3.budget_line bl ON bl.id = rd.line_id WHERE rd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN RAISE EXCEPTION 'rent_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id; END IF; ELSIF p_sheet = 'AHR_UTILITY' THEN IF NOT EXISTS (SELECT 1 FROM v3.utility_detail ud JOIN v3.budget_line bl ON bl.id = ud.line_id WHERE ud.id = v_line_id AND bl.budget_form_id = p_form_id) THEN RAISE EXCEPTION 'utility_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id; END IF; ELSIF p_sheet = 'AHR_SECURITY' THEN IF NOT EXISTS (SELECT 1 FROM v3.security_detail sd JOIN v3.budget_line bl ON bl.id = sd.line_id WHERE sd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN RAISE EXCEPTION 'security_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id; END IF; ELSE IF NOT EXISTS (SELECT 1 FROM v3.budget_line WHERE id = v_line_id AND budget_form_id = p_form_id) THEN RAISE EXCEPTION 'budget_line id=% не принадлежит budget_form id=%', v_line_id, p_form_id; END IF; END IF; -- Сначала ТОЛЬКО регистрируем v_changed_lines, не применяем (для snapshot -- before нужно знать все затронутые до изменения). IF NOT (v_line_id = ANY(v_changed_lines)) THEN v_changed_lines := v_changed_lines || v_line_id; END IF; END LOOP; -- ── Вычисляем path-set для каждого типа листа (ОДИН раз, для before и after) IF p_sheet = 'AHR_LIMIT' THEN v_lc_path := v3._limit_template_path_set(v_changed_lines); ELSIF p_sheet NOT IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY','OTCH9F') THEN SELECT array_agg(DISTINCT bl.expense_item_id) INTO v_eid_set FROM v3.budget_line bl WHERE bl.id = ANY(v_changed_lines); v_ei_path := v3._expense_item_path_set(v_eid_set); END IF; -- ── Snapshot BEFORE (тот же фильтр что и для возврата) ──────────────── IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb) INTO v_before FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(v_changed_lines); ELSIF p_sheet = 'AHR_LIMIT' THEN SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb) INTO v_before FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v WHERE (v.data->>'id')::INT = ANY(v_lc_path); ELSIF p_sheet = 'OTCH9F' THEN SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb) INTO v_before FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v WHERE EXISTS ( SELECT 1 FROM v3.fixed_asset_report far WHERE far.id = ANY(v_changed_lines) AND far.expense_item_id = (v.data->>'expense_item_id')::INT AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name')); ELSE SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb) INTO v_before FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) 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 = ANY(v_changed_lines)); END IF; -- ── Применяем изменения ───────────────────────────────────────────── FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes) LOOP PERFORM v3._apply_form_cell( (v_change->>'line_id')::INT, v_change->>'column', v_change->'value', p_sheet, p_form_id); END LOOP; -- ── Возврат: AFTER vs BEFORE diff. Только строки с непустым diff. -- Identifier-ключи (line_id / header.expense_item_id / id) гарантированно -- добавлены в diff, чтобы фронт мог match с своим state. IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN RETURN QUERY WITH after_rows AS ( SELECT v.row_type, v.depth, v.sort_order, v.data, v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(v_changed_lines) ) SELECT a.row_type, a.depth, a.sort_order, a.diff || jsonb_build_object('id', (a.data->>'id')::INT) FROM after_rows a WHERE a.diff <> '{}'::jsonb ORDER BY a.sort_order; ELSIF p_sheet = 'AHR_LIMIT' THEN RETURN QUERY WITH after_rows AS ( SELECT v.row_type, v.depth, v.sort_order, v.data, v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v WHERE (v.data->>'id')::INT = ANY(v_lc_path) ) SELECT a.row_type, a.depth, a.sort_order, a.diff || jsonb_build_object('id', (a.data->>'id')::INT) FROM after_rows a WHERE a.diff <> '{}'::jsonb ORDER BY a.sort_order; ELSIF p_sheet = 'OTCH9F' THEN RETURN QUERY WITH after_rows AS ( SELECT v.row_type, v.depth, v.sort_order, v.data, v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v WHERE EXISTS ( SELECT 1 FROM v3.fixed_asset_report far WHERE far.id = ANY(v_changed_lines) AND far.expense_item_id = (v.data->>'expense_item_id')::INT AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name')) ) SELECT a.row_type, a.depth, a.sort_order, a.diff || jsonb_build_object('id', (a.data->>'id')::INT) FROM after_rows a WHERE a.diff <> '{}'::jsonb ORDER BY a.sort_order; ELSE -- Основная сетка FORM_1/2/4 RETURN QUERY WITH after_rows AS ( SELECT v.row_type, v.depth, v.sort_order, v.data, v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) 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 = ANY(v_changed_lines)) ) SELECT a.row_type, a.depth, a.sort_order, CASE WHEN a.row_type = 'INPUT' THEN a.diff || jsonb_build_object('line_id', (a.data->>'line_id')::INT) ELSE -- иерархия: добавить header.expense_item_id если diff его не содержит a.diff || jsonb_build_object('header', COALESCE(a.diff->'header', '{}'::jsonb) || jsonb_build_object('expense_item_id', (a.data->'header'->>'expense_item_id')::INT)) END FROM after_rows a WHERE a.diff <> '{}'::jsonb ORDER BY a.sort_order; END IF; END; $function$ ; -- DROP FUNCTION v3.upd_form_cells(int4, varchar, jsonb, int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_user_id integer, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql AS $function$ DECLARE v_change JSONB; v_column TEXT; v_decision JSONB; v_msg TEXT; BEGIN IF p_user_id IS NULL THEN RAISE EXCEPTION 'role_not_allowed: '; END IF; IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id; END IF; IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN RAISE EXCEPTION 'p_changes must be a JSONB array'; END IF; -- Проверяем КАЖДУЮ ячейку до начала записи. Падаем на первой запрещённой — -- транзакция целиком откатится, частичной записи не будет. FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes) LOOP v_column := v_change->>'column'; IF v_column IS NULL THEN RAISE EXCEPTION 'change must have column: %', v_change; END IF; v_decision := v3.can_edit(p_form_id, p_sheet, v_column, p_user_id); IF NOT (v_decision->>'ok')::boolean THEN IF v_decision->>'code' = 'role_not_allowed' THEN RAISE EXCEPTION 'role_not_allowed: %', v_column; ELSIF v_decision->>'code' = 'org_not_assigned' THEN RAISE EXCEPTION 'org_not_assigned: %', v_column; ELSIF v_decision->>'code' = 'window_closed' THEN IF v_decision ? 'opens_at' THEN v_msg := format('window_closed: %s (opens %s)', v_column, v_decision->>'opens_at'); ELSIF v_decision->>'closes_at' IS NOT NULL THEN v_msg := format('window_closed: %s (closed %s)', v_column, v_decision->>'closes_at'); ELSE v_msg := format('window_closed: %s (no phase)', v_column); END IF; RAISE EXCEPTION '%', v_msg; ELSE RAISE EXCEPTION '%: %', v_decision->>'code', v_column; END IF; END IF; END LOOP; -- Все ячейки разрешены — делегируем в v3. -- ROW_UPDATE намеренно НЕ логируется (см. logging_events.md): cell-edit'ы -- слишком частые. В audit_log пишем только структурные события -- (ROW_CREATE / ROW_DELETE) + бизнес-операции (CREATE_TASK и т.д.). RETURN QUERY SELECT * FROM v3.upd_form_cells( p_form_id, p_sheet, p_changes, p_direction, p_sections ); END; $function$ ; -- DROP FUNCTION v3.upd_form_cell(int4, varchar, int4, text, jsonb, varchar, _text); CREATE OR REPLACE FUNCTION v3.upd_form_cell(p_form_id integer, p_sheet character varying, p_budget_line_id integer, p_column text, p_value jsonb, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql AS $function$ SELECT * FROM v3.upd_form_cells( p_form_id, p_sheet, jsonb_build_array(jsonb_build_object( 'line_id', p_budget_line_id, 'column', p_column, 'value', p_value )), p_direction, p_sections ); $function$ ; -- DROP FUNCTION v3.upd_form_cell(int4, varchar, int4, text, jsonb, int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.upd_form_cell(p_form_id integer, p_sheet character varying, p_budget_line_id integer, p_column text, p_value jsonb, p_user_id integer, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql AS $function$ SELECT * FROM v3.upd_form_cells( p_form_id, p_sheet, jsonb_build_array(jsonb_build_object( 'line_id', p_budget_line_id, 'column', p_column, 'value', p_value )), p_user_id, p_direction, p_sections ); $function$ ; -- DROP FUNCTION v3.upd_form_phase(int4, varchar, varchar, varchar, _text, timestamptz, timestamptz); CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp with time zone DEFAULT NULL::timestamp with time zone, p_closes_at timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS v3.form_phase LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form_phase; v_new v3.form_phase; v_is_extend BOOLEAN := FALSE; v_has_non_extend_change BOOLEAN := FALSE; v_has_window_change BOOLEAN := FALSE; v_changes JSONB := '{}'::jsonb; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; BEGIN SELECT * INTO v_old FROM v3.form_phase WHERE budget_form_id = p_budget_form_id AND sheet = p_sheet AND phase_code = p_phase_code; IF NOT FOUND THEN RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)', p_budget_form_id, p_sheet, p_phase_code; END IF; UPDATE v3.form_phase SET role = COALESCE(p_role, role), column_keys = COALESCE(p_column_keys, column_keys), opens_at = COALESCE(p_opens_at, opens_at), closes_at = COALESCE(p_closes_at, closes_at) WHERE budget_form_id = p_budget_form_id AND sheet = p_sheet AND phase_code = p_phase_code RETURNING * INTO v_new; SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year FROM v3.budget_form bf LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id WHERE bf.id = p_budget_form_id; v_task_name := trim(concat( COALESCE(v_form_type_code, ''), CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code); v_is_extend := v_new.closes_at > v_old.closes_at; v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role OR v_old.column_keys IS DISTINCT FROM v_new.column_keys OR v_old.opens_at IS DISTINCT FROM v_new.opens_at; v_has_window_change := v_has_non_extend_change OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend); IF v_old.role IS DISTINCT FROM v_new.role THEN v_changes := v_changes || jsonb_build_object( 'role', jsonb_build_object('before', v_old.role, 'after', v_new.role) ); END IF; IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN v_changes := v_changes || jsonb_build_object( 'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys) ); END IF; IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN v_changes := v_changes || jsonb_build_object( 'opens_at', jsonb_build_object( 'before', v_old.opens_at, 'after', v_new.opens_at ) ); END IF; IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN v_changes := v_changes || jsonb_build_object( 'closes_at', jsonb_build_object( 'before', v_old.closes_at, 'after', v_new.closes_at ) ); END IF; IF v_is_extend THEN PERFORM v3.log_event( 'ACCESS_EXTEND', 'ACCESS', jsonb_build_object( 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'task_name', NULLIF(v_task_name, ''), 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'closes_at_before', v_old.closes_at, 'closes_at_after', v_new.closes_at ), p_budget_form_id ); END IF; IF v_has_window_change THEN PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'form_phase', 'entity_id', p_budget_form_id, 'action', 'update', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_budget_form_id, 'task_name', NULLIF(v_task_name, ''), 'form_id', p_budget_form_id, 'budget_form_id', p_budget_form_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'sheet', p_sheet, 'phase_code', p_phase_code, 'changes', v_changes ), p_budget_form_id, null, v_org_unit_id ); END IF; RETURN v_new; END; $function$ ; -- DROP FUNCTION v3.upd_project(int4, text, jsonb); CREATE OR REPLACE FUNCTION v3.upd_project(p_project_id integer, p_column text, p_value jsonb) RETURNS jsonb LANGUAGE plpgsql AS $function$ DECLARE v_target_col TEXT; v_target_type TEXT; v_sql TEXT; v_out JSONB; BEGIN IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN RAISE EXCEPTION 'project id=% не существует', p_project_id; END IF; -- Editable атрибуты (CHECK-ы из migrate_form3_v1.sql применятся при UPDATE). -- Не editable: level (структура), parent_id (иерархия). CASE p_column WHEN 'name' THEN v_target_col := 'name'; v_target_type := 'TEXT'; WHEN 'project_type' THEN v_target_col := 'project_type'; v_target_type := 'TEXT'; WHEN 'vsp_format' THEN v_target_col := 'vsp_format'; v_target_type := 'TEXT'; WHEN 'placement_type' THEN v_target_col := 'placement_type'; v_target_type := 'TEXT'; WHEN 'object_address' THEN v_target_col := 'object_address'; v_target_type := 'TEXT'; WHEN 'staff_count' THEN v_target_col := 'staff_count'; v_target_type := 'INTEGER'; WHEN 'total_area' THEN v_target_col := 'total_area'; v_target_type := 'NUMERIC'; WHEN 'org_unit_id' THEN v_target_col := 'org_unit_id'; v_target_type := 'INTEGER'; WHEN 'level','parent_id' THEN RAISE EXCEPTION 'structural_field: % (правится отдельно через ADD project / структурный API)', p_column; WHEN 'id' THEN RAISE EXCEPTION 'key_field: id (RO)'; ELSE RAISE EXCEPTION 'unknown_column: %', p_column; END CASE; -- FK санити (если применимо) IF v_target_col = 'org_unit_id' AND p_value <> 'null'::jsonb THEN IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = (p_value #>> '{}')::INT) THEN RAISE EXCEPTION 'org_unit id=% не существует', (p_value #>> '{}'); END IF; END IF; v_sql := format( 'UPDATE v3.project SET %1$I = ($2 #>> ''{}'')::%2$s WHERE id = $1', v_target_col, v_target_type ); EXECUTE v_sql USING p_project_id, p_value; -- Возврат — весь header проекта SELECT jsonb_build_object( 'id', p.id, 'name', p.name, '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, 'staff_count', p.staff_count, 'total_area', p.total_area ) INTO v_out FROM v3.project p WHERE p.id = p_project_id; RETURN v_out; END; $function$ ; -- DROP FUNCTION v3.upd_user(int4, varchar, varchar, varchar, varchar, int4, bool); CREATE OR REPLACE FUNCTION v3.upd_user(p_user_id integer, p_email character varying DEFAULT NULL::character varying, p_username character varying DEFAULT NULL::character varying, p_hashed_password character varying DEFAULT NULL::character varying, p_full_name character varying DEFAULT NULL::character varying, p_role_id integer DEFAULT NULL::integer, p_is_active boolean DEFAULT NULL::boolean) RETURNS v3.app_user LANGUAGE plpgsql AS $function$ DECLARE v_old_role_id INT; v_new v3.app_user; BEGIN SELECT role_id INTO v_old_role_id FROM v3.app_user WHERE id = p_user_id; IF NOT FOUND THEN RAISE EXCEPTION 'app_user #% не существует', p_user_id; END IF; UPDATE v3.app_user SET email = COALESCE(p_email, email), username = COALESCE(p_username, username), hashed_password = COALESCE(p_hashed_password, hashed_password), full_name = COALESCE(p_full_name, full_name), role_id = COALESCE(p_role_id, role_id), is_active = COALESCE(p_is_active, is_active), updated_at = now() WHERE id = p_user_id RETURNING * INTO v_new; IF v_old_role_id IS DISTINCT FROM v_new.role_id THEN PERFORM v3.log_event( 'USER_ROLE_CHANGE', 'USER', jsonb_build_object( 'user_id', p_user_id, 'entity_id', p_user_id, 'email', v_new.email, 'full_name', v_new.full_name, 'role_id_before', v_old_role_id, 'role_id_after', v_new.role_id ) ); END IF; RETURN v_new; END; $function$ ; -- DROP FUNCTION v3.upd_vsp(int4, int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, date); CREATE OR REPLACE FUNCTION v3.upd_vsp(p_vsp_id integer, p_branch_id integer DEFAULT NULL::integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT NULL::boolean, p_is_deleted boolean DEFAULT NULL::boolean, p_system_code character varying DEFAULT NULL::character varying, p_updated_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE v_old RECORD; v_changes JSONB := '{}'::jsonb; v_branch_name_before VARCHAR; v_branch_name_after VARCHAR; v_actor_id INT; v_actor_email VARCHAR; v_actor_full_name VARCHAR; BEGIN SELECT v.branch_id, v.reg_number, v.address, v.format, v.opened_at, v.placement_type, v.staff_count, v.total_area, v.closed_at, v.is_active, v.is_deleted, v.system_code, v.vsp_type, v.notes, v.rent_contract_num, v.rent_end_date INTO v_old FROM v3.vsp v WHERE v.id = p_vsp_id; IF NOT FOUND THEN RAISE EXCEPTION 'vsp #% не существует', p_vsp_id; END IF; SELECT ou.title INTO v_branch_name_before FROM v3.org_unit ou WHERE ou.id = v_old.branch_id; v_branch_name_after := v_branch_name_before; v_actor_id := COALESCE(p_updated_by, v3.current_user_id()); IF v_actor_id IS NOT NULL THEN SELECT u.email, u.full_name INTO v_actor_email, v_actor_full_name FROM v3.app_user u WHERE u.id = v_actor_id; END IF; IF p_branch_id IS NOT NULL AND p_branch_id IS DISTINCT FROM v_old.branch_id THEN SELECT ou.title INTO v_branch_name_after FROM v3.org_unit ou WHERE ou.id = p_branch_id; UPDATE v3.vsp SET branch_id = p_branch_id WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object( 'branch_id', jsonb_build_object( 'before', v3._diff_val(v_old.branch_id), 'after', v3._diff_val(p_branch_id) ), 'branch_name', jsonb_build_object( 'before', v3._diff_val(v_branch_name_before), 'after', v3._diff_val(v_branch_name_after) ) ); END IF; IF p_reg_number IS NOT NULL AND p_reg_number IS DISTINCT FROM v_old.reg_number THEN UPDATE v3.vsp SET reg_number = p_reg_number WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('reg_number', jsonb_build_object('before', v3._diff_val(v_old.reg_number), 'after', v3._diff_val(p_reg_number))); END IF; IF p_address IS NOT NULL AND p_address IS DISTINCT FROM v_old.address THEN UPDATE v3.vsp SET address = p_address WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('address', jsonb_build_object('before', v3._diff_val(v_old.address), 'after', v3._diff_val(p_address))); END IF; IF p_format IS NOT NULL AND p_format IS DISTINCT FROM v_old.format THEN UPDATE v3.vsp SET format = p_format WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('format', jsonb_build_object('before', v3._diff_val(v_old.format), 'after', v3._diff_val(p_format))); END IF; IF p_opened_at IS NOT NULL AND p_opened_at IS DISTINCT FROM v_old.opened_at THEN UPDATE v3.vsp SET opened_at = p_opened_at WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('opened_at', jsonb_build_object('before', v3._diff_val(v_old.opened_at), 'after', v3._diff_val(p_opened_at))); END IF; IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN UPDATE v3.vsp SET placement_type = p_placement_type WHERE id = p_vsp_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_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN UPDATE v3.vsp SET staff_count = p_staff_count WHERE id = p_vsp_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.vsp SET total_area = p_total_area WHERE id = p_vsp_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_closed_at IS NOT NULL AND p_closed_at IS DISTINCT FROM v_old.closed_at THEN UPDATE v3.vsp SET closed_at = p_closed_at WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('closed_at', jsonb_build_object('before', v3._diff_val(v_old.closed_at), 'after', v3._diff_val(p_closed_at))); END IF; IF p_is_active IS NOT NULL AND p_is_active IS DISTINCT FROM v_old.is_active THEN UPDATE v3.vsp SET is_active = p_is_active WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('is_active', jsonb_build_object('before', v3._diff_val(v_old.is_active), 'after', v3._diff_val(p_is_active))); END IF; IF p_is_deleted IS NOT NULL AND p_is_deleted IS DISTINCT FROM v_old.is_deleted THEN UPDATE v3.vsp SET is_deleted = p_is_deleted WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('is_deleted', jsonb_build_object('before', v3._diff_val(v_old.is_deleted), 'after', v3._diff_val(p_is_deleted))); END IF; IF p_system_code IS NOT NULL AND p_system_code IS DISTINCT FROM v_old.system_code THEN UPDATE v3.vsp SET system_code = p_system_code WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('system_code', jsonb_build_object('before', v3._diff_val(v_old.system_code), 'after', v3._diff_val(p_system_code))); END IF; IF p_vsp_type IS NOT NULL AND p_vsp_type IS DISTINCT FROM v_old.vsp_type THEN UPDATE v3.vsp SET vsp_type = p_vsp_type WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('vsp_type', jsonb_build_object('before', v3._diff_val(v_old.vsp_type), 'after', v3._diff_val(p_vsp_type))); END IF; IF p_notes IS NOT NULL AND p_notes IS DISTINCT FROM v_old.notes THEN UPDATE v3.vsp SET notes = p_notes WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('notes', jsonb_build_object('before', v3._diff_val(v_old.notes), 'after', v3._diff_val(p_notes))); END IF; IF p_rent_contract_num IS NOT NULL AND p_rent_contract_num IS DISTINCT FROM v_old.rent_contract_num THEN UPDATE v3.vsp SET rent_contract_num = p_rent_contract_num WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('rent_contract_num', jsonb_build_object('before', v3._diff_val(v_old.rent_contract_num), 'after', v3._diff_val(p_rent_contract_num))); END IF; IF p_rent_end_date IS NOT NULL AND p_rent_end_date IS DISTINCT FROM v_old.rent_end_date THEN UPDATE v3.vsp SET rent_end_date = p_rent_end_date WHERE id = p_vsp_id; v_changes := v_changes || jsonb_build_object('rent_end_date', jsonb_build_object('before', v3._diff_val(v_old.rent_end_date), 'after', v3._diff_val(p_rent_end_date))); END IF; IF v_changes <> '{}'::jsonb THEN UPDATE v3.vsp SET updated_at = now(), updated_by = p_updated_by WHERE id = p_vsp_id; PERFORM v3.log_event( 'VSP_UPDATE', 'VSP', jsonb_build_object( 'vsp_id', p_vsp_id, 'entity_id', p_vsp_id, 'branch_id', COALESCE(p_branch_id, v_old.branch_id), 'branch_name', COALESCE(v_branch_name_after, v_branch_name_before), 'changes', v_changes, 'user_email', v_actor_email, 'user_full_name', v_actor_full_name ), null, null, COALESCE(p_branch_id, v_old.branch_id) ); END IF; END; $function$ ; -- DROP FUNCTION v3.user_in_form_org(int4, int4); CREATE OR REPLACE FUNCTION v3.user_in_form_org(p_user_id integer, p_form_id integer) RETURNS boolean LANGUAGE plpgsql STABLE AS $function$ DECLARE v_org INT; BEGIN SELECT org_unit_id INTO v_org FROM v3.budget_form WHERE id = p_form_id; IF v_org IS NULL THEN RETURN TRUE; -- форма не привязана к org_unit — пропускаем фильтр END IF; RETURN EXISTS ( SELECT 1 FROM v3.user_org WHERE user_id = p_user_id AND org_unit_id = v_org ); END; $function$ ; -- DROP FUNCTION v3.user_role_code(int4); CREATE OR REPLACE FUNCTION v3.user_role_code(p_user_id integer) RETURNS character varying LANGUAGE plpgsql STABLE AS $function$ DECLARE v_code VARCHAR; BEGIN SELECT r.code INTO v_code FROM v3.app_user u JOIN v3.role r ON r.id = u.role_id WHERE u.id = p_user_id AND u.is_active; RETURN v_code; -- NULL если юзер не найден или неактивен END; $function$ ; -- DROP FUNCTION v3.v_form1_sheet_sections(int4, varchar, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form1_sheet_sections(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_b_year integer, col_c_section character varying, col_d_item_id character varying, col_e_num_group character varying, col_f_name character varying, col_g_internal_order character varying, col_h_plan_q1 numeric, col_i_plan_q2 numeric, col_j_plan_q3 numeric, col_k_plan_q4 numeric, col_l_plan_year numeric, col_m_plan_comment character varying, col_n_cs_total numeric, col_o_cs_counterparty character varying, col_p_cs_deadline character varying, col_q_cs_comment character varying, col_r_cs_future_y1 numeric, col_s_cs_future_y2 numeric, col_t_cs_other_ssp numeric, col_v_al_order character varying, col_w_al_property character varying, col_y_seq_q1 numeric, col_z_seq_q2 numeric, col_aa_seq_q3 numeric, col_ab_seq_q4 numeric, col_ac_seq_year numeric, col_ad_seq_justification character varying, col_af_res_q1 numeric, col_ag_res_q2 numeric, col_ah_res_q3 numeric, col_ai_res_q4 numeric, col_aj_res_year numeric, col_ak_res_justification character varying, col_am_approved_q1 numeric, col_an_approved_q2 numeric, col_ao_approved_q3 numeric, col_ap_approved_q4 numeric, col_aq_approved_year numeric, col_as_ca_approved numeric, col_at_ca_protocol character varying, col_au_ca_note character varying, col_aw_ckk_ceiling numeric, col_ax_ckk_exp_q1 numeric, col_ay_ckk_exp_q2 numeric, col_az_ckk_exp_q3 numeric, col_ba_ckk_exp_q4 numeric, col_bb_ckk_rf_sched character varying, col_bc_ckk_deadline character varying, col_bd_ckk_proc_plan character varying, col_be_ckk_proc_meth character varying, col_bf_ckk_comment character varying, col_bh_cd_counterparty character varying, col_bi_cd_reference character varying, col_bj_cd_addenda character varying, col_bk_cd_subject character varying, col_bl_cd_currency character varying, col_bm_cd_ceiling numeric, col_bn_cd_exp_q1 numeric, col_bo_cd_exp_q2 numeric, col_bp_cd_exp_q3 numeric, col_bq_cd_exp_q4 numeric, col_br_cd_rf_sched character varying, col_bs_cd_vat character varying, col_bt_cd_rate numeric, col_bu_cd_amount_fx numeric, col_bv_cd_deadline character varying, col_bw_cd_scheme character varying, col_bx_cd_act character varying, col_by_cd_comment character varying, col_ca_q1_adj_current numeric, col_cb_q1_adj_ssp numeric, col_cc_q1_adj_rf numeric, col_cd_q1_adj_reserve numeric, col_ce_q1_adj_comment character varying, col_cf_q1_corrected_plan numeric, col_ch_q1_pay_date date, col_ci_q1_pay_amount numeric, col_cj_q1_pay_ho numeric, col_ck_q1_pay_rf numeric, col_cl_q1_pay_comment character varying, col_cm_q1_pay_act character varying, col_co_q1_booking numeric, col_cp_q1_actual_m1 numeric, col_cq_q1_actual_m2 numeric, col_cr_q1_actual_m3 numeric, col_cs_q1_actual_quarter numeric, col_ct_q1_rem_booking numeric, col_cu_q1_rem_actual numeric, col_cw_q1_trans_q2 numeric, col_cx_q1_trans_q3 numeric, col_cy_q1_trans_q4 numeric, col_cz_q1_economy numeric, col_da_q1_total numeric, col_dc_q2_rev_eco numeric, col_dd_q2_rev_item numeric, col_de_q2_rev_inc numeric, col_df_q2_rev_seq numeric, col_dg_q2_rev_comment character varying, col_dh_q2_new_plan numeric, col_dj_q2_adj_current numeric, col_dk_q2_adj_ssp numeric, col_dl_q2_adj_rf numeric, col_dm_q2_adj_reserve numeric, col_dn_q2_adj_comment character varying, col_do_q2_corrected_plan numeric, col_dq_q2_pay_date date, col_dr_q2_pay_amount numeric, col_ds_q2_pay_ho numeric, col_dt_q2_pay_rf numeric, col_du_q2_pay_comment character varying, col_dv_q2_pay_act character varying, col_dx_q2_booking numeric, col_dy_q2_actual_m1 numeric, col_dz_q2_actual_m2 numeric, col_ea_q2_actual_m3 numeric, col_eb_q2_actual_quarter numeric, col_ec_q2_rem_booking numeric, col_ed_q2_rem_actual numeric, col_ef_q2_trans_q3 numeric, col_eg_q2_trans_q4 numeric, col_eh_q2_economy numeric, col_ei_q2_total numeric, col_ek_q3_rev_eco numeric, col_el_q3_rev_item numeric, col_em_q3_rev_inc numeric, col_en_q3_rev_seq numeric, col_eo_q3_rev_comment character varying, col_ep_q3_new_plan numeric, col_er_q3_adj_current numeric, col_es_q3_adj_ssp numeric, col_et_q3_adj_rf numeric, col_eu_q3_adj_reserve numeric, col_ev_q3_adj_comment character varying, col_ew_q3_corrected_plan numeric, col_ey_q3_pay_date date, col_ez_q3_pay_amount numeric, col_fa_q3_pay_ho numeric, col_fb_q3_pay_rf numeric, col_fc_q3_pay_comment character varying, col_fd_q3_pay_act character varying, col_ff_q3_booking numeric, col_fg_q3_actual_m1 numeric, col_fh_q3_actual_m2 numeric, col_fi_q3_actual_m3 numeric, col_fj_q3_actual_quarter numeric, col_fk_q3_rem_booking numeric, col_fl_q3_rem_actual numeric, col_fn_q3_trans_q4 numeric, col_fo_q3_economy numeric, col_fp_q3_total numeric, col_fr_q4_rev_eco numeric, col_fs_q4_rev_item numeric, col_ft_q4_rev_inc numeric, col_fu_q4_rev_seq numeric, col_fv_q4_rev_comment character varying, col_fw_q4_new_plan numeric, col_fy_q4_adj_current numeric, col_fz_q4_adj_ssp numeric, col_ga_q4_adj_rf numeric, col_gb_q4_adj_reserve numeric, col_gc_q4_adj_comment character varying, col_gd_q4_corrected_plan numeric, col_gf_q4_pay_date date, col_gg_q4_pay_amount numeric, col_gh_q4_pay_ho numeric, col_gi_q4_pay_rf numeric, col_gj_q4_pay_comment character varying, col_gk_q4_pay_act character varying, col_gm_q4_booking numeric, col_gn_q4_actual_m1 numeric, col_go_q4_actual_m2 numeric, col_gp_q4_actual_m3 numeric, col_gq_q4_actual_quarter numeric, col_gr_q4_actual_spod numeric, col_gs_q4_rem_booking numeric, col_gt_q4_rem_actual numeric, col_gv_q4_economy numeric, col_gw_q4_total numeric, col_gz_fact_year numeric, col_hb_pay_year numeric, _sort_path integer[]) LANGUAGE plpgsql STABLE SET search_path TO 'v3', 'pg_catalog' AS $function$ #variable_conflict use_column DECLARE v_year INT; s_plan BOOL; s_cs BOOL; s_al BOOL; s_seq BOOL; s_res BOOL; s_appr BOOL; s_ca BOOL; s_ckk BOOL; s_cd BOOL; s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; s_any_q BOOL; s_need_ap BOOL; -- plan/seq/res нужны: approved И/ИЛИ cp1..cp4 s_need_bk BOOL; -- ckk/cd нужны: booking bk1..bk4 (в Q-секциях) BEGIN SELECT bf.year INTO v_year FROM budget_form bf WHERE bf.id = p_form_id; s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); s_cs := p_sections IS NULL OR 'contract_summary' = ANY(p_sections); s_al := p_sections IS NULL OR 'allocation' = ANY(p_sections); s_seq := p_sections IS NULL OR 'sequestration' = ANY(p_sections); s_res := p_sections IS NULL OR 'reserve' = ANY(p_sections); s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); s_ca := p_sections IS NULL OR 'collegial' = ANY(p_sections); s_ckk := p_sections IS NULL OR 'ckk' = ANY(p_sections); s_cd := p_sections IS NULL OR 'contract_detail' = ANY(p_sections); s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); s_any_q := s_q1 OR s_q2 OR s_q3 OR s_q4 OR s_tot; s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; s_need_bk := s_q1 OR s_q2 OR s_q3 OR s_q4; RETURN QUERY WITH -- ═══ 1. Дерево expense_item из materialized view ══════════════════════ -- JOIN с expense_item_form_type фильтрует узлы по form_type текущей формы tw AS ( SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids FROM mv_expense_item_tree t JOIN expense_item_form_type eft ON eft.expense_item_id = t.id JOIN budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code WHERE t.sheet = p_sheet ), -- ═══ 2. Все INPUT строки формы ══════════════════════════════════════════ -- p_direction: фильтр по budget_line.direction. NULL = без фильтра. -- bl.direction IS NULL обходит фильтр (для листов без direction, напр. Операц). page AS ( SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.internal_order AS io, bl.name AS bname FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (p_direction IS NULL OR bl.direction IS NULL OR bl.direction = p_direction) ), -- ═══ 3. Satellite JOINs (условные) ════════════════════════════════════ jp AS (SELECT p.* FROM plan p JOIN page pg ON p.line_id = pg.lid WHERE s_plan), jcs AS (SELECT cs.* FROM contract_summary cs JOIN page pg ON cs.line_id = pg.lid WHERE s_cs), jal AS (SELECT al.* FROM allocation al JOIN page pg ON al.line_id = pg.lid WHERE s_al), jsd AS (SELECT sd.* FROM sequestration sd JOIN page pg ON sd.line_id = pg.lid WHERE s_seq AND sd.actor='DFIP'), jr AS (SELECT r.* FROM reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_res), jca AS (SELECT ca.* FROM collegial_approval ca JOIN page pg ON ca.line_id = pg.lid WHERE s_ca), jck AS (SELECT ck.* FROM ckk ck JOIN page pg ON ck.line_id = pg.lid WHERE s_ckk), jcd AS (SELECT cd.* FROM contract_detail cd JOIN page pg ON cd.line_id = pg.lid WHERE s_cd), -- Direct quarter joins (no pivot — WHERE quarter=N) jq1 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q1 AND blq.quarter = 1), jq2 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q2 AND blq.quarter = 2), jq3 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q3 AND blq.quarter = 3), jq4 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE (s_q4 OR s_tot) AND blq.quarter = 4), -- For totals: need all 4 quarters to sum actual across year — but only when s_tot and not all q loaded jtot AS ( SELECT blq.line_id AS tlid, SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq1, SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq2, SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq3, SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS tot_aq4, SUM(CASE WHEN blq.quarter=1 THEN blq.payment_amount END) AS tot_pa1, SUM(CASE WHEN blq.quarter=2 THEN blq.payment_amount END) AS tot_pa2, SUM(CASE WHEN blq.quarter=3 THEN blq.payment_amount END) AS tot_pa3, SUM(CASE WHEN blq.quarter=4 THEN blq.payment_amount END) AS tot_pa4 FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_tot GROUP BY blq.line_id ), -- ═══ 4. INPUT строки: enriched с computed ═════════════════════════════ -- Промежуточные вычисления для computed-полей input_base AS ( SELECT pg.lid, pg.eid, pg.io, pg.bname, -- section_code = parent's item_id (hierarchy code like 1.01.1.) t.parent_item_id AS sc, -- item_id = R-code for depth=3, else hierarchy code t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, -- raw satellite values (for computed) COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, p.comment AS pcmt, COALESCE(sd.adj_q1,0) AS sq1, COALESCE(sd.adj_q2,0) AS sq2, COALESCE(sd.adj_q3,0) AS sq3, COALESCE(sd.adj_q4,0) AS sq4, sd.justification AS sjust, COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, r.justification AS rjust, cs.*, al.internal_order AS al_io, al.property_object AS al_po, ca.approved_amount AS ca_amt, ca.protocol_reference AS ca_prot, ca.note AS ca_note, ck.ceiling_amount AS ckk_ceil, COALESCE(ck.expenses_q1,0) AS ckk1, COALESCE(ck.expenses_q2,0) AS ckk2, COALESCE(ck.expenses_q3,0) AS ckk3, COALESCE(ck.expenses_q4,0) AS ckk4, ck.rf_schedule AS ckk_rf, ck.delivery_deadline AS ckk_dl, ck.procurement_plan AS ckk_pp, ck.procurement_method AS ckk_pm, ck.comment AS ckk_cmt, cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.addenda AS cd_add, cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil, COALESCE(cd.expenses_q1,0) AS cdq1, COALESCE(cd.expenses_q2,0) AS cdq2, COALESCE(cd.expenses_q3,0) AS cdq3, COALESCE(cd.expenses_q4,0) AS cdq4, cd.rf_schedule AS cd_rf, cd.vat_rate AS cd_vat, cd.exchange_rate AS cd_rate, cd.amount_foreign AS cd_fx, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, cd.act AS cd_act, cd.comment AS cd_cmt2, -- Q1 direct q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_rf AS q1_ar, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt, q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, q1.payment_comment AS q1_pcmt, q1.payment_act AS q1_pact, q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, -- Q2 direct q2.plan_revision_eco_change AS q2_reco, q2.plan_revision_item_adj AS q2_ritm, q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, q2.plan_revision_comment AS q2_rcmt, q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_rf AS q2_ar, q2.adj_reserve AS q2_arv, q2.adj_comment AS q2_acmt, q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf, q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, -- Q3 direct q3.plan_revision_eco_change AS q3_reco, q3.plan_revision_item_adj AS q3_ritm, q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, q3.plan_revision_comment AS q3_rcmt, q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_rf AS q3_ar, q3.adj_reserve AS q3_arv, q3.adj_comment AS q3_acmt, q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf, q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, q3.transfer_to_q4 AS q3_tq4, -- Q4 direct q4.plan_revision_eco_change AS q4_reco, q4.plan_revision_item_adj AS q4_ritm, q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, q4.plan_revision_comment AS q4_rcmt, q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_rf AS q4_ar, q4.adj_reserve AS q4_arv, q4.adj_comment AS q4_acmt, q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf, q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod, -- totals tot.tot_aq1, tot.tot_aq2, tot.tot_aq3, tot.tot_aq4, tot.tot_pa1, tot.tot_pa2, tot.tot_pa3, tot.tot_pa4, -- booking per quarter CASE WHEN COALESCE(cd.expenses_q1,0)>0 THEN cd.expenses_q1 ELSE ck.expenses_q1 END AS bk1, CASE WHEN COALESCE(cd.expenses_q2,0)>0 THEN cd.expenses_q2 ELSE ck.expenses_q2 END AS bk2, CASE WHEN COALESCE(cd.expenses_q3,0)>0 THEN cd.expenses_q3 ELSE ck.expenses_q3 END AS bk3, CASE WHEN COALESCE(cd.expenses_q4,0)>0 THEN cd.expenses_q4 ELSE ck.expenses_q4 END AS bk4, -- approved per quarter COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4, -- tree path for ordering t.path AS tree_path FROM page pg JOIN tw t ON t.id = pg.eid LEFT JOIN jp p ON p.line_id = pg.lid LEFT JOIN jcs cs ON cs.line_id = pg.lid LEFT JOIN jal al ON al.line_id = pg.lid LEFT JOIN jsd sd ON sd.line_id = pg.lid LEFT JOIN jr r ON r.line_id = pg.lid LEFT JOIN jca ca ON ca.line_id = pg.lid LEFT JOIN jck ck ON ck.line_id = pg.lid LEFT JOIN jcd cd ON cd.line_id = pg.lid LEFT JOIN jq1 q1 ON q1.line_id = pg.lid LEFT JOIN jq2 q2 ON q2.line_id = pg.lid LEFT JOIN jq3 q3 ON q3.line_id = pg.lid LEFT JOIN jq4 q4 ON q4.line_id = pg.lid LEFT JOIN jtot tot ON tot.tlid = pg.lid ), -- Q1-Q4 computed fields enriched AS ( SELECT b.*, -- Q1 corrected_plan b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_ar,0)+COALESCE(b.q1_arv,0) AS cp1, -- Q1 actual_quarter COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, -- Q2 new_plan b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_reco,0)+COALESCE(b.q2_ritm,0)+COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) AS np2, -- Q2 actual_quarter COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, -- Q3 new_plan b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_reco,0)+COALESCE(b.q3_ritm,0)+COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) AS np3, -- Q3 actual_quarter COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, -- Q4 new_plan b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_reco,0)+COALESCE(b.q4_ritm,0)+COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) AS np4, -- Q4 actual_quarter (без СПОД) COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0) AS aq4_no_spod, -- Q4 actual with СПОД COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 FROM input_base b ), -- Final computed: corrected_plan Qn = new_plan + adj, rem/economy/total final_input AS ( SELECT e.*, -- Q2 corrected_plan e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_ar,0)+COALESCE(e.q2_arv,0) AS cp2, -- Q3 corrected_plan e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_ar,0)+COALESCE(e.q3_arv,0) AS cp3, -- Q4 corrected_plan e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_ar,0)+COALESCE(e.q4_arv,0) AS cp4 FROM enriched e ), -- ═══ 5. Агрегаты для иерархии ═════════════════════════════════════════ agg AS ( SELECT bl.expense_item_id AS eid, SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, SUM(sd.adj_q1) AS ss1, SUM(sd.adj_q2) AS ss2, SUM(sd.adj_q3) AS ss3, SUM(sd.adj_q4) AS ss4, SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, SUM(ca.approved_amount) AS sca, SUM(ck.ceiling_amount) AS sckc, SUM(ck.expenses_q1) AS sck1, SUM(ck.expenses_q2) AS sck2, SUM(ck.expenses_q3) AS sck3, SUM(ck.expenses_q4) AS sck4, SUM(cd.ceiling_amount) AS scdc, SUM(cd.expenses_q1) AS scd1, SUM(cd.expenses_q2) AS scd2, SUM(cd.expenses_q3) AS scd3, SUM(cd.expenses_q4) AS scd4, SUM(cs.total_amount) AS scst, SUM(cs.future_payments_y1) AS sfy1, SUM(cs.future_payments_y2) AS sfy2, SUM(cs.other_ssp_amount) AS sossp FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN plan p ON p.line_id = bl.id AND s_need_ap LEFT JOIN contract_summary cs ON cs.line_id = bl.id AND s_cs LEFT JOIN sequestration sd ON sd.line_id = bl.id AND sd.actor = 'DFIP' AND s_need_ap LEFT JOIN reserve r ON r.line_id = bl.id AND s_need_ap LEFT JOIN collegial_approval ca ON ca.line_id = bl.id AND s_ca LEFT JOIN ckk ck ON ck.line_id = bl.id AND (s_ckk OR s_need_bk) LEFT JOIN contract_detail cd ON cd.line_id = bl.id AND (s_cd OR s_need_bk) WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) AND (s_need_ap OR s_cs OR s_ca OR s_ckk OR s_cd OR s_need_bk) GROUP BY bl.expense_item_id ), -- Per-quarter hierarchy aggregates (no pivot — direct WHERE quarter=N) aq1 AS ( SELECT bl.expense_item_id AS eid, SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, SUM(blq.transfer_to_q2) AS tq2, SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 1 WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q1 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) GROUP BY bl.expense_item_id ), aq2 AS ( SELECT bl.expense_item_id AS eid, SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 2 WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q2 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) GROUP BY bl.expense_item_id ), aq3 AS ( SELECT bl.expense_item_id AS eid, SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, SUM(blq.transfer_to_q4) AS tq4 FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 3 WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q3 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) GROUP BY bl.expense_item_id ), aq4 AS ( SELECT bl.expense_item_id AS eid, SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, SUM(blq.actual_spod) AS spod FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 4 WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_q4 OR s_tot) AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) GROUP BY bl.expense_item_id ), atot AS ( SELECT bl.expense_item_id AS eid, SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq1, SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq2, SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq3, SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS fq4, SUM(blq.payment_amount) AS pay FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_tot AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) GROUP BY bl.expense_item_id ), -- ═══ Pre-computed per-node aggregates (one pass over tw × agg) ═════════ tw_agg AS ( SELECT tw.id, SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, SUM(a.ss1) AS ss1, SUM(a.ss2) AS ss2, SUM(a.ss3) AS ss3, SUM(a.ss4) AS ss4, SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, SUM(a.sca) AS sca, SUM(a.sckc) AS sckc, SUM(a.sck1) AS sck1, SUM(a.sck2) AS sck2, SUM(a.sck3) AS sck3, SUM(a.sck4) AS sck4, SUM(a.scdc) AS scdc, SUM(a.scd1) AS scd1, SUM(a.scd2) AS scd2, SUM(a.scd3) AS scd3, SUM(a.scd4) AS scd4, SUM(a.scst) AS scst, SUM(a.sfy1) AS sfy1, SUM(a.sfy2) AS sfy2, SUM(a.sossp) AS sossp, SUM(CASE WHEN COALESCE(a.scd1,0)>0 THEN a.scd1 ELSE a.sck1 END) AS bk1, SUM(CASE WHEN COALESCE(a.scd2,0)>0 THEN a.scd2 ELSE a.sck2 END) AS bk2, SUM(CASE WHEN COALESCE(a.scd3,0)>0 THEN a.scd3 ELSE a.sck3 END) AS bk3, SUM(CASE WHEN COALESCE(a.scd4,0)>0 THEN a.scd4 ELSE a.sck4 END) AS bk4 FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) GROUP BY tw.id ), tw_aq1 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4 FROM tw LEFT JOIN aq1 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id ), tw_aq2 AS ( SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4 FROM tw LEFT JOIN aq2 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id ), tw_aq3 AS ( SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq4) AS tq4 FROM tw LEFT JOIN aq3 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id ), tw_aq4 AS ( SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod FROM tw LEFT JOIN aq4 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id ), tw_atot AS ( SELECT tw.id, SUM(b.fq1) AS fq1, SUM(b.fq2) AS fq2, SUM(b.fq3) AS fq3, SUM(b.fq4) AS fq4, SUM(b.pay) AS pay FROM tw LEFT JOIN atot b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id ) -- ═══ ЧАСТЬ A: INPUT строки ════════════════════════════════════════════ SELECT * FROM ( SELECT 'INPUT'::VARCHAR, 3, f.lid, v_year, f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.io, CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, CASE WHEN s_cs THEN f.total_amount END, CASE WHEN s_cs THEN f.counterparty END, CASE WHEN s_cs THEN f.deadline END, CASE WHEN s_cs THEN f.comment END, CASE WHEN s_cs THEN f.future_payments_y1 END, CASE WHEN s_cs THEN f.future_payments_y2 END, CASE WHEN s_cs THEN f.other_ssp_amount END, CASE WHEN s_al THEN f.al_io END, CASE WHEN s_al THEN f.al_po END, CASE WHEN s_seq THEN f.sq1 END, CASE WHEN s_seq THEN f.sq2 END, CASE WHEN s_seq THEN f.sq3 END, CASE WHEN s_seq THEN f.sq4 END, CASE WHEN s_seq THEN f.sq1+f.sq2+f.sq3+f.sq4 END, CASE WHEN s_seq THEN f.sjust END, CASE WHEN s_res THEN f.rq1 END, CASE WHEN s_res THEN f.rq2 END, CASE WHEN s_res THEN f.rq3 END, CASE WHEN s_res THEN f.rq4 END, CASE WHEN s_res THEN f.rq1+f.rq2+f.rq3+f.rq4 END, CASE WHEN s_res THEN f.rjust END, CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, CASE WHEN s_ca THEN f.ca_amt END, CASE WHEN s_ca THEN f.ca_prot END, CASE WHEN s_ca THEN f.ca_note END, CASE WHEN s_ckk THEN f.ckk_ceil END, CASE WHEN s_ckk THEN f.ckk1 END, CASE WHEN s_ckk THEN f.ckk2 END, CASE WHEN s_ckk THEN f.ckk3 END, CASE WHEN s_ckk THEN f.ckk4 END, CASE WHEN s_ckk THEN f.ckk_rf END, CASE WHEN s_ckk THEN f.ckk_dl END, CASE WHEN s_ckk THEN f.ckk_pp END, CASE WHEN s_ckk THEN f.ckk_pm END, CASE WHEN s_ckk THEN f.ckk_cmt END, CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, CASE WHEN s_cd THEN f.cd_add END, CASE WHEN s_cd THEN f.cd_subj END, CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, CASE WHEN s_cd THEN f.cdq1 END, CASE WHEN s_cd THEN f.cdq2 END, CASE WHEN s_cd THEN f.cdq3 END, CASE WHEN s_cd THEN f.cdq4 END, CASE WHEN s_cd THEN f.cd_rf END, CASE WHEN s_cd THEN f.cd_vat END, CASE WHEN s_cd THEN f.cd_rate END, CASE WHEN s_cd THEN f.cd_fx END, CASE WHEN s_cd THEN f.cd_dl END, CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END, CASE WHEN s_cd THEN f.cd_cmt2 END, -- Q1 CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, CASE WHEN s_q1 THEN f.q1_ar END, CASE WHEN s_q1 THEN f.q1_arv END, CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, CASE WHEN s_q1 THEN f.q1_pcmt END, CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN COALESCE(f.bk1,0) END, CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, CASE WHEN s_q1 THEN f.aq1 END, CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.bk1,0) END, CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, CASE WHEN s_q1 THEN f.q1_tq2 END, CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, CASE WHEN s_q1 THEN f.cp1 - f.aq1 - COALESCE(f.q1_tq2,0) - COALESCE(f.q1_tq3,0) - COALESCE(f.q1_tq4,0) END, CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, -- Q2 CASE WHEN s_q2 THEN f.q2_reco END, CASE WHEN s_q2 THEN f.q2_ritm END, CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, CASE WHEN s_q2 THEN f.q2_rcmt END, CASE WHEN s_q2 THEN f.np2 END, CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, CASE WHEN s_q2 THEN f.q2_ar END, CASE WHEN s_q2 THEN f.q2_arv END, CASE WHEN s_q2 THEN f.q2_acmt END, CASE WHEN s_q2 THEN f.cp2 END, CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END, CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, CASE WHEN s_q2 THEN COALESCE(f.bk2,0) END, CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, CASE WHEN s_q2 THEN f.aq2 END, CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.bk2,0) END, CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, CASE WHEN s_q2 THEN f.q2_tq3 END, CASE WHEN s_q2 THEN f.q2_tq4 END, CASE WHEN s_q2 THEN f.cp2 - f.aq2 - COALESCE(f.q2_tq3,0) - COALESCE(f.q2_tq4,0) END, CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, -- Q3 CASE WHEN s_q3 THEN f.q3_reco END, CASE WHEN s_q3 THEN f.q3_ritm END, CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, CASE WHEN s_q3 THEN f.q3_rcmt END, CASE WHEN s_q3 THEN f.np3 END, CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, CASE WHEN s_q3 THEN f.q3_ar END, CASE WHEN s_q3 THEN f.q3_arv END, CASE WHEN s_q3 THEN f.q3_acmt END, CASE WHEN s_q3 THEN f.cp3 END, CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END, CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, CASE WHEN s_q3 THEN COALESCE(f.bk3,0) END, CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, CASE WHEN s_q3 THEN f.aq3 END, CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.bk3,0) END, CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, CASE WHEN s_q3 THEN f.q3_tq4 END, CASE WHEN s_q3 THEN f.cp3 - f.aq3 - COALESCE(f.q3_tq4,0) END, CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, -- Q4 CASE WHEN s_q4 THEN f.q4_reco END, CASE WHEN s_q4 THEN f.q4_ritm END, CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, CASE WHEN s_q4 THEN f.q4_rcmt END, CASE WHEN s_q4 THEN f.np4 END, CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, CASE WHEN s_q4 THEN f.q4_ar END, CASE WHEN s_q4 THEN f.q4_arv END, CASE WHEN s_q4 THEN f.q4_acmt END, CASE WHEN s_q4 THEN f.cp4 END, CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END, CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, CASE WHEN s_q4 THEN COALESCE(f.bk4,0) END, CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, CASE WHEN s_q4 THEN f.aq4_no_spod END, CASE WHEN s_q4 THEN f.q4_spod END, CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.bk4,0) END, CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, -- totals CASE WHEN s_tot THEN COALESCE(f.tot_aq1,0)+COALESCE(f.tot_aq2,0)+COALESCE(f.tot_aq3,0)+COALESCE(f.tot_aq4,0) END, CASE WHEN s_tot THEN COALESCE(f.tot_pa1,0)+COALESCE(f.tot_pa2,0)+COALESCE(f.tot_pa3,0)+COALESCE(f.tot_pa4,0) END, f.tree_path || ARRAY[f.lid] AS _sort FROM final_input f UNION ALL -- ═══ ЧАСТЬ B: Строки иерархии ═════════════════════════════════════════ SELECT CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR, t.depth, NULL::INT, v_year, -- col_C = hierarchy code for depth 0-2, parent's code for depth 3 (SUB_ITEM) CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END, -- col_D = NULL for hierarchy, R-code for SUB_ITEM CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL::VARCHAR END, t.num_group_id, t.name, NULL::VARCHAR, -- plan CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END, CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END, CASE WHEN s_plan THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0)) END, NULL::VARCHAR, -- contract_summary CASE WHEN s_cs THEN ta.scst END, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, CASE WHEN s_cs THEN ta.sfy1 END, CASE WHEN s_cs THEN ta.sfy2 END, CASE WHEN s_cs THEN ta.sossp END, NULL::VARCHAR, NULL::VARCHAR, -- sequestration CASE WHEN s_seq THEN ta.ss1 END, CASE WHEN s_seq THEN ta.ss2 END, CASE WHEN s_seq THEN ta.ss3 END, CASE WHEN s_seq THEN ta.ss4 END, CASE WHEN s_seq THEN (COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0)) END, NULL::VARCHAR, -- reserve CASE WHEN s_res THEN ta.sr1 END, CASE WHEN s_res THEN ta.sr2 END, CASE WHEN s_res THEN ta.sr3 END, CASE WHEN s_res THEN ta.sr4 END, CASE WHEN s_res THEN (COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END, NULL::VARCHAR, -- approved CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) END, CASE WHEN s_appr THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) END, CASE WHEN s_appr THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) END, CASE WHEN s_appr THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) END, CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) +COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0) +COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END, -- collegial CASE WHEN s_ca THEN ta.sca END, NULL::VARCHAR, NULL::VARCHAR, -- ckk CASE WHEN s_ckk THEN ta.sckc END, CASE WHEN s_ckk THEN ta.sck1 END, CASE WHEN s_ckk THEN ta.sck2 END, CASE WHEN s_ckk THEN ta.sck3 END, CASE WHEN s_ckk THEN ta.sck4 END, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- contract_detail NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, CASE WHEN s_cd THEN ta.scdc END, CASE WHEN s_cd THEN ta.scd1 END, CASE WHEN s_cd THEN ta.scd2 END, CASE WHEN s_cd THEN ta.scd3 END, CASE WHEN s_cd THEN ta.scd4 END, NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- Q1 hierarchy (from aq1) CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as2 END, CASE WHEN s_q1 THEN tb1.ar END, CASE WHEN s_q1 THEN tb1.arv END, NULL::VARCHAR, -- corrected_plan Q1 = approved_q1 + SUM(adj_*) CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)), 0) END, NULL::DATE, CASE WHEN s_q1 THEN tb1.pa END, CASE WHEN s_q1 THEN tb1.pho END, CASE WHEN s_q1 THEN tb1.prf END, NULL::VARCHAR, NULL::VARCHAR, -- booking Q1 = SUM(CASE cd>0 THEN cd ELSE ckk) CASE WHEN s_q1 THEN ta.bk1 END, CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END, CASE WHEN s_q1 THEN (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) END, -- rem_booking Q1 = corrected_plan - booking (ckk or cd) CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) - COALESCE(ta.bk1,0) END, -- rem_actual Q1 = corrected_plan - actual_quarter CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) END, CASE WHEN s_q1 THEN tb1.tq2 END, CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END, -- economy Q1 = rem_actual - transfers CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) - COALESCE((COALESCE(tb1.tq2,0)+COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)),0) END, -- total Q1 = rem_actual CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) END, -- Q2 hierarchy (from aq2) CASE WHEN s_q2 THEN tb2.reco END, CASE WHEN s_q2 THEN tb2.ritm END, CASE WHEN s_q2 THEN tb2.rinc END, CASE WHEN s_q2 THEN tb2.rseq END, NULL::VARCHAR, -- new_plan Q2 = approved_q2 + transfer_q1→q2 + revisions CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + COALESCE(tb1.tq2, 0) + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)), 0) END, CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as2 END, CASE WHEN s_q2 THEN tb2.ar END, CASE WHEN s_q2 THEN tb2.arv END, NULL::VARCHAR, -- corrected_plan Q2 = new_plan_q2 + adj_* CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + COALESCE(tb1.tq2, 0) + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)), 0) END, NULL::DATE, CASE WHEN s_q2 THEN tb2.pa END, CASE WHEN s_q2 THEN tb2.pho END, CASE WHEN s_q2 THEN tb2.prf END, NULL::VARCHAR, NULL::VARCHAR, CASE WHEN s_q2 THEN ta.bk2 END, CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END, CASE WHEN s_q2 THEN (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) END, -- rem_booking Q2 CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + COALESCE(tb1.tq2,0) + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) - COALESCE(ta.bk2,0) END, -- rem_actual Q2 CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + COALESCE(tb1.tq2,0) + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) END, CASE WHEN s_q2 THEN tb2.tq3 END, CASE WHEN s_q2 THEN tb2.tq4 END, -- economy Q2 CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + COALESCE(tb1.tq2,0) + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) - COALESCE((COALESCE(tb2.tq3,0)+COALESCE(tb2.tq4,0)),0) END, -- total Q2 = rem_actual CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + COALESCE(tb1.tq2,0) + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) END, -- Q3 hierarchy (from aq3) CASE WHEN s_q3 THEN tb3.reco END, CASE WHEN s_q3 THEN tb3.ritm END, CASE WHEN s_q3 THEN tb3.rinc END, CASE WHEN s_q3 THEN tb3.rseq END, NULL::VARCHAR, -- new_plan Q3 = approved_q3 + transfer_q1→q3 + transfer_q2→q3 + revisions CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + COALESCE(tb1.tq3, 0) + COALESCE(tb2.tq3, 0) + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)), 0) END, CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as2 END, CASE WHEN s_q3 THEN tb3.ar END, CASE WHEN s_q3 THEN tb3.arv END, NULL::VARCHAR, -- corrected_plan Q3 = new_plan + adj_* CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + COALESCE(tb1.tq3, 0) + COALESCE(tb2.tq3, 0) + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)), 0) END, NULL::DATE, CASE WHEN s_q3 THEN tb3.pa END, CASE WHEN s_q3 THEN tb3.pho END, CASE WHEN s_q3 THEN tb3.prf END, NULL::VARCHAR, NULL::VARCHAR, CASE WHEN s_q3 THEN ta.bk3 END, CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END, CASE WHEN s_q3 THEN (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) END, -- rem_booking Q3 CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + COALESCE(tb1.tq3,0) + COALESCE(tb2.tq3,0) + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) - COALESCE(ta.bk3,0) END, -- rem_actual Q3 CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + COALESCE(tb1.tq3,0) + COALESCE(tb2.tq3,0) + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) END, CASE WHEN s_q3 THEN tb3.tq4 END, -- economy Q3 CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + COALESCE(tb1.tq3,0) + COALESCE(tb2.tq3,0) + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) - COALESCE(COALESCE(tb3.tq4,0),0) END, -- total Q3 = rem_actual CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + COALESCE(tb1.tq3,0) + COALESCE(tb2.tq3,0) + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) END, -- Q4 hierarchy (from aq4) CASE WHEN s_q4 THEN tb4.reco END, CASE WHEN s_q4 THEN tb4.ritm END, CASE WHEN s_q4 THEN tb4.rinc END, CASE WHEN s_q4 THEN tb4.rseq END, NULL::VARCHAR, -- new_plan Q4 = approved_q4 + transfers_q1→q4 + q2→q4 + q3→q4 + revisions CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + COALESCE(tb1.tq4, 0) + COALESCE(tb2.tq4, 0) + COALESCE(tb3.tq4, 0) + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)), 0) END, CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as2 END, CASE WHEN s_q4 THEN tb4.ar END, CASE WHEN s_q4 THEN tb4.arv END, NULL::VARCHAR, -- corrected_plan Q4 = new_plan + adj_* CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + COALESCE(tb1.tq4, 0) + COALESCE(tb2.tq4, 0) + COALESCE(tb3.tq4, 0) + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)), 0) END, NULL::DATE, CASE WHEN s_q4 THEN tb4.pa END, CASE WHEN s_q4 THEN tb4.pho END, CASE WHEN s_q4 THEN tb4.prf END, NULL::VARCHAR, NULL::VARCHAR, CASE WHEN s_q4 THEN ta.bk4 END, CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END, CASE WHEN s_q4 THEN (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)) END, CASE WHEN s_q4 THEN tb4.spod END, -- rem_booking Q4 CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + COALESCE(tb1.tq4,0) + COALESCE(tb2.tq4,0) + COALESCE(tb3.tq4,0) + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) - COALESCE(ta.bk4,0) END, -- rem_actual Q4 (with spod) CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + COALESCE(tb1.tq4,0) + COALESCE(tb2.tq4,0) + COALESCE(tb3.tq4,0) + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) END, -- economy Q4 = rem_actual (no transfers from Q4) CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + COALESCE(tb1.tq4,0) + COALESCE(tb2.tq4,0) + COALESCE(tb3.tq4,0) + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) END, -- total Q4 = rem_actual CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + COALESCE(tb1.tq4,0) + COALESCE(tb2.tq4,0) + COALESCE(tb3.tq4,0) + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) END, -- totals hierarchy (from atot) CASE WHEN s_tot THEN (COALESCE(tt.fq1,0)+COALESCE(tt.fq2,0)+COALESCE(tt.fq3,0)+COALESCE(tt.fq4,0)) END, CASE WHEN s_tot THEN tt.pay END, t.path AS _sort FROM tw t LEFT JOIN tw_agg ta ON ta.id = t.id LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id LEFT JOIN tw_atot tt ON tt.id = t.id ) sub ORDER BY sub._sort; END; $function$ ; -- DROP FUNCTION v3.v_form1_sheet_jsonb(int4, varchar, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form1_sheet_jsonb(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, contract_summary jsonb, allocation_data jsonb, sequestration_data jsonb, reserve_data jsonb, approved_data jsonb, collegial_data jsonb, ckk_data jsonb, contract_detail jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[]) LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog' AS $function$ WITH flags AS ( SELECT (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs, (p_sections IS NULL OR 'allocation' = ANY(p_sections)) AS s_al, (p_sections IS NULL OR 'sequestration' = ANY(p_sections)) AS s_seq, (p_sections IS NULL OR 'reserve' = ANY(p_sections)) AS s_res, (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, (p_sections IS NULL OR 'collegial' = ANY(p_sections)) AS s_ca, (p_sections IS NULL OR 'ckk' = ANY(p_sections)) AS s_ckk, (p_sections IS NULL OR 'contract_detail' = ANY(p_sections)) AS s_cd, (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot ) SELECT s.row_type, s.depth, s.line_id, -- header (всегда). expense_item_id извлекается из _sort_path: -- для INPUT путь = {...ei_id, line_id} → предпоследний элемент -- для иерархии путь = {...ei_id} → последний элемент jsonb_build_object( 'year', s.col_B_year, 'section', s.col_C_section, 'item_id', s.col_D_item_id, 'num_group', s.col_E_num_group, 'name', s.col_F_name, 'internal_order', s.col_G_internal_order, 'expense_item_id', CASE WHEN s.row_type = 'INPUT' THEN s._sort_path[array_upper(s._sort_path,1)-1] ELSE s._sort_path[array_upper(s._sort_path,1)] END ) AS header, CASE WHEN f.s_plan THEN jsonb_build_object( 'q1', s.col_H_plan_q1, 'q2', s.col_I_plan_q2, 'q3', s.col_J_plan_q3, 'q4', s.col_K_plan_q4, 'year', s.col_L_plan_year, 'comment', s.col_M_plan_comment ) END AS plan_data, CASE WHEN f.s_cs THEN jsonb_build_object( 'total', s.col_N_cs_total, 'counterparty', s.col_O_cs_counterparty, 'deadline', s.col_P_cs_deadline, 'comment', s.col_Q_cs_comment, 'future_y1', s.col_R_cs_future_y1, 'future_y2', s.col_S_cs_future_y2, 'other_ssp', s.col_T_cs_other_ssp ) END AS contract_summary, CASE WHEN f.s_al THEN jsonb_build_object( 'order', s.col_V_al_order, 'property', s.col_W_al_property ) END AS allocation_data, CASE WHEN f.s_seq THEN jsonb_build_object( 'q1', s.col_Y_seq_q1, 'q2', s.col_Z_seq_q2, 'q3', s.col_AA_seq_q3, 'q4', s.col_AB_seq_q4, 'year', s.col_AC_seq_year, 'justification', s.col_AD_seq_justification ) END AS sequestration_data, CASE WHEN f.s_res THEN jsonb_build_object( 'q1', s.col_AF_res_q1, 'q2', s.col_AG_res_q2, 'q3', s.col_AH_res_q3, 'q4', s.col_AI_res_q4, 'year', s.col_AJ_res_year, 'justification', s.col_AK_res_justification ) END AS reserve_data, CASE WHEN f.s_appr THEN jsonb_build_object( 'q1', s.col_AM_approved_q1, 'q2', s.col_AN_approved_q2, 'q3', s.col_AO_approved_q3, 'q4', s.col_AP_approved_q4, 'year', s.col_AQ_approved_year ) END AS approved_data, CASE WHEN f.s_ca THEN jsonb_build_object( 'approved', s.col_AS_ca_approved, 'protocol', s.col_AT_ca_protocol, 'note', s.col_AU_ca_note ) END AS collegial_data, CASE WHEN f.s_ckk THEN jsonb_build_object( 'ceiling', s.col_AW_ckk_ceiling, 'q1', s.col_AX_ckk_exp_q1, 'q2', s.col_AY_ckk_exp_q2, 'q3', s.col_AZ_ckk_exp_q3, 'q4', s.col_BA_ckk_exp_q4, 'rf_schedule', s.col_BB_ckk_rf_sched, 'deadline', s.col_BC_ckk_deadline, 'proc_plan', s.col_BD_ckk_proc_plan, 'proc_method', s.col_BE_ckk_proc_meth, 'comment', s.col_BF_ckk_comment ) END AS ckk_data, CASE WHEN f.s_cd THEN jsonb_build_object( 'counterparty', s.col_BH_cd_counterparty, 'reference', s.col_BI_cd_reference, 'addenda', s.col_BJ_cd_addenda, 'subject', s.col_BK_cd_subject, 'currency', s.col_BL_cd_currency, 'ceiling', s.col_BM_cd_ceiling, 'q1', s.col_BN_cd_exp_q1, 'q2', s.col_BO_cd_exp_q2, 'q3', s.col_BP_cd_exp_q3, 'q4', s.col_BQ_cd_exp_q4, 'rf_schedule', s.col_BR_cd_rf_sched, 'vat_rate', s.col_BS_cd_vat, 'exchange_rate', s.col_BT_cd_rate, 'amount_foreign', s.col_BU_cd_amount_fx, 'deadline', s.col_BV_cd_deadline, 'payment_scheme', s.col_BW_cd_scheme, 'act', s.col_BX_cd_act, 'comment', s.col_BY_cd_comment ) END AS contract_detail, CASE WHEN f.s_q1 THEN jsonb_build_object( 'adj_current', s.col_CA_q1_adj_current, 'adj_ssp', s.col_CB_q1_adj_ssp, 'adj_rf', s.col_CC_q1_adj_rf, 'adj_reserve', s.col_CD_q1_adj_reserve, 'adj_comment', s.col_CE_q1_adj_comment, 'corrected_plan', s.col_CF_q1_corrected_plan, 'pay_date', s.col_CH_q1_pay_date, 'pay_amount', s.col_CI_q1_pay_amount, 'pay_ho', s.col_CJ_q1_pay_ho, 'pay_rf', s.col_CK_q1_pay_rf, 'pay_comment', s.col_CL_q1_pay_comment, 'pay_act', s.col_CM_q1_pay_act, 'booking', s.col_CO_q1_booking, 'actual_m1', s.col_CP_q1_actual_m1, 'actual_m2', s.col_CQ_q1_actual_m2, 'actual_m3', s.col_CR_q1_actual_m3, 'actual_quarter', s.col_CS_q1_actual_quarter, 'rem_booking', s.col_CT_q1_rem_booking, 'rem_actual', s.col_CU_q1_rem_actual, 'transfer_q2', s.col_CW_q1_trans_q2, 'transfer_q3', s.col_CX_q1_trans_q3, 'transfer_q4', s.col_CY_q1_trans_q4, 'economy', s.col_CZ_q1_economy, 'total', s.col_DA_q1_total ) END AS q1_data, CASE WHEN f.s_q2 THEN jsonb_build_object( 'rev_eco', s.col_DC_q2_rev_eco, 'rev_item', s.col_DD_q2_rev_item, 'rev_inc', s.col_DE_q2_rev_inc, 'rev_seq', s.col_DF_q2_rev_seq, 'rev_comment', s.col_DG_q2_rev_comment, 'new_plan', s.col_DH_q2_new_plan, 'adj_current', s.col_DJ_q2_adj_current, 'adj_ssp', s.col_DK_q2_adj_ssp, 'adj_rf', s.col_DL_q2_adj_rf, 'adj_reserve', s.col_DM_q2_adj_reserve, 'adj_comment', s.col_DN_q2_adj_comment, 'corrected_plan', s.col_DO_q2_corrected_plan, 'pay_date', s.col_DQ_q2_pay_date, 'pay_amount', s.col_DR_q2_pay_amount, 'pay_ho', s.col_DS_q2_pay_ho, 'pay_rf', s.col_DT_q2_pay_rf, 'pay_comment', s.col_DU_q2_pay_comment, 'pay_act', s.col_DV_q2_pay_act, 'booking', s.col_DX_q2_booking, 'actual_m1', s.col_DY_q2_actual_m1, 'actual_m2', s.col_DZ_q2_actual_m2, 'actual_m3', s.col_EA_q2_actual_m3, 'actual_quarter', s.col_EB_q2_actual_quarter, 'rem_booking', s.col_EC_q2_rem_booking, 'rem_actual', s.col_ED_q2_rem_actual, 'transfer_q3', s.col_EF_q2_trans_q3, 'transfer_q4', s.col_EG_q2_trans_q4, 'economy', s.col_EH_q2_economy, 'total', s.col_EI_q2_total ) END AS q2_data, CASE WHEN f.s_q3 THEN jsonb_build_object( 'rev_eco', s.col_EK_q3_rev_eco, 'rev_item', s.col_EL_q3_rev_item, 'rev_inc', s.col_EM_q3_rev_inc, 'rev_seq', s.col_EN_q3_rev_seq, 'rev_comment', s.col_EO_q3_rev_comment, 'new_plan', s.col_EP_q3_new_plan, 'adj_current', s.col_ER_q3_adj_current, 'adj_ssp', s.col_ES_q3_adj_ssp, 'adj_rf', s.col_ET_q3_adj_rf, 'adj_reserve', s.col_EU_q3_adj_reserve, 'adj_comment', s.col_EV_q3_adj_comment, 'corrected_plan', s.col_EW_q3_corrected_plan, 'pay_date', s.col_EY_q3_pay_date, 'pay_amount', s.col_EZ_q3_pay_amount, 'pay_ho', s.col_FA_q3_pay_ho, 'pay_rf', s.col_FB_q3_pay_rf, 'pay_comment', s.col_FC_q3_pay_comment, 'pay_act', s.col_FD_q3_pay_act, 'booking', s.col_FF_q3_booking, 'actual_m1', s.col_FG_q3_actual_m1, 'actual_m2', s.col_FH_q3_actual_m2, 'actual_m3', s.col_FI_q3_actual_m3, 'actual_quarter', s.col_FJ_q3_actual_quarter, 'rem_booking', s.col_FK_q3_rem_booking, 'rem_actual', s.col_FL_q3_rem_actual, 'transfer_q4', s.col_FN_q3_trans_q4, 'economy', s.col_FO_q3_economy, 'total', s.col_FP_q3_total ) END AS q3_data, CASE WHEN f.s_q4 THEN jsonb_build_object( 'rev_eco', s.col_FR_q4_rev_eco, 'rev_item', s.col_FS_q4_rev_item, 'rev_inc', s.col_FT_q4_rev_inc, 'rev_seq', s.col_FU_q4_rev_seq, 'rev_comment', s.col_FV_q4_rev_comment, 'new_plan', s.col_FW_q4_new_plan, 'adj_current', s.col_FY_q4_adj_current, 'adj_ssp', s.col_FZ_q4_adj_ssp, 'adj_rf', s.col_GA_q4_adj_rf, 'adj_reserve', s.col_GB_q4_adj_reserve, 'adj_comment', s.col_GC_q4_adj_comment, 'corrected_plan', s.col_GD_q4_corrected_plan, 'pay_date', s.col_GF_q4_pay_date, 'pay_amount', s.col_GG_q4_pay_amount, 'pay_ho', s.col_GH_q4_pay_ho, 'pay_rf', s.col_GI_q4_pay_rf, 'pay_comment', s.col_GJ_q4_pay_comment, 'pay_act', s.col_GK_q4_pay_act, 'booking', s.col_GM_q4_booking, 'actual_m1', s.col_GN_q4_actual_m1, 'actual_m2', s.col_GO_q4_actual_m2, 'actual_m3', s.col_GP_q4_actual_m3, 'actual_quarter', s.col_GQ_q4_actual_quarter, 'actual_spod', s.col_GR_q4_actual_spod, 'rem_booking', s.col_GS_q4_rem_booking, 'rem_actual', s.col_GT_q4_rem_actual, 'economy', s.col_GV_q4_economy, 'total', s.col_GW_q4_total ) END AS q4_data, CASE WHEN f.s_tot THEN jsonb_build_object( 'fact_year', s.col_GZ_fact_year, 'pay_year', s.col_HB_pay_year ) END AS totals_data, s._sort_path FROM v_form1_sheet_sections(p_form_id, p_sheet, p_direction, p_sections) s CROSS JOIN flags f ORDER BY s._sort_path; $function$ ; -- DROP FUNCTION v3.v_form1_smeta(int4, int4); CREATE OR REPLACE FUNCTION v3.v_form1_smeta(p_year integer, p_org_unit_id integer) RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, supp_plan_q1 numeric, supp_plan_q2 numeric, supp_plan_q3 numeric, supp_plan_q4 numeric, supp_plan_year numeric, dev_plan_q1 numeric, dev_plan_q2 numeric, dev_plan_q3 numeric, dev_plan_q4 numeric, dev_plan_year numeric, total_plan_year numeric, supp_appr_q1 numeric, supp_appr_q2 numeric, supp_appr_q3 numeric, supp_appr_q4 numeric, supp_appr_year numeric, dev_appr_q1 numeric, dev_appr_q2 numeric, dev_appr_q3 numeric, dev_appr_q4 numeric, dev_appr_year numeric, total_appr_year numeric, supp_act_q1 numeric, supp_act_q2 numeric, supp_act_q3 numeric, supp_act_q4 numeric, supp_act_year numeric, dev_act_q1 numeric, dev_act_q2 numeric, dev_act_q3 numeric, dev_act_q4 numeric, dev_act_year numeric, total_act_year numeric, supp_corr_q2 numeric, supp_corr_q3 numeric, supp_corr_q4 numeric, dev_corr_q2 numeric, dev_corr_q3 numeric, dev_corr_q4 numeric) LANGUAGE plpgsql STABLE SET search_path TO 'v3', 'pg_catalog' AS $function$ #variable_conflict use_column DECLARE v_ahr INT; v_cap INT; v_oper INT; v_sections TEXT[] := ARRAY['plan','approved','q1','q2','q3','q4']; BEGIN -- Одна budget_form на лист содержит ОБА направления (budget_line.direction). -- Support/Development различаются параметром p_direction, а не отдельной формой. WITH ranked AS ( SELECT bf.id AS fid, ei.sheet AS sh FROM budget_form bf JOIN budget_line bl ON bl.budget_form_id = bf.id JOIN expense_item ei ON ei.id = bl.expense_item_id WHERE bf.year = p_year AND bf.org_unit_id = p_org_unit_id GROUP BY bf.id, ei.sheet ) SELECT MAX(CASE WHEN sh='AHR' THEN fid END), MAX(CASE WHEN sh='CAP' THEN fid END), MAX(CASE WHEN sh='OPER' THEN fid END) INTO v_ahr, v_cap, v_oper FROM ranked; RETURN QUERY WITH ahr_s AS ( SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type, col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr, col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr, col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3, COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4, col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4 FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Support', v_sections) WHERE row_type IN ('ROOT','GROUP','ITEM') ), ahr_d AS ( SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type, col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr, col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr, col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3, COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4, col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4 FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Development', v_sections) WHERE row_type IN ('ROOT','GROUP','ITEM') ), cap_s AS ( SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type, col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr, col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr, col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3, COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4, col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4 FROM v_form1_sheet_sections(v_cap, 'CAP', 'Support', v_sections) WHERE row_type IN ('ROOT','GROUP','ITEM') ), cap_d AS ( SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type, col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr, col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr, col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3, COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4, col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4 FROM v_form1_sheet_sections(v_cap, 'CAP', 'Development', v_sections) WHERE row_type IN ('ROOT','GROUP','ITEM') ), oper_s AS ( SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type, col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr, col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr, col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3, COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4, col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4 FROM v_form1_sheet_sections(v_oper, 'OPER', 'Support', v_sections) WHERE row_type IN ('ROOT','GROUP','ITEM') ), merged AS ( SELECT COALESCE(s.row_type, d.row_type) AS row_type, COALESCE(s.depth, d.depth) AS depth, COALESCE(s.sc, d.sc) AS sc, COALESCE(s.nm, d.nm) AS nm, s.pq1, s.pq2, s.pq3, s.pq4, s.pyr, d.pq1 AS d_pq1, d.pq2 AS d_pq2, d.pq3 AS d_pq3, d.pq4 AS d_pq4, d.pyr AS d_pyr, s.aq1, s.aq2, s.aq3, s.aq4, s.ayr, d.aq1 AS d_aq1, d.aq2 AS d_aq2, d.aq3 AS d_aq3, d.aq4 AS d_aq4, d.ayr AS d_ayr, s.fq1, s.fq2, s.fq3, s.fq4, d.fq1 AS d_fq1, d.fq2 AS d_fq2, d.fq3 AS d_fq3, d.fq4 AS d_fq4, s.np2, s.np3, s.np4, d.np2 AS d_np2, d.np3 AS d_np3, d.np4 AS d_np4 FROM ahr_s s FULL OUTER JOIN ahr_d d ON s.sc = d.sc UNION ALL SELECT COALESCE(s.row_type, d.row_type), COALESCE(s.depth, d.depth), COALESCE(s.sc, d.sc), COALESCE(s.nm, d.nm), s.pq1, s.pq2, s.pq3, s.pq4, s.pyr, d.pq1, d.pq2, d.pq3, d.pq4, d.pyr, s.aq1, s.aq2, s.aq3, s.aq4, s.ayr, d.aq1, d.aq2, d.aq3, d.aq4, d.ayr, s.fq1, s.fq2, s.fq3, s.fq4, d.fq1, d.fq2, d.fq3, d.fq4, s.np2, s.np3, s.np4, d.np2, d.np3, d.np4 FROM cap_s s FULL OUTER JOIN cap_d d ON s.sc = d.sc UNION ALL SELECT s.row_type, s.depth, s.sc, s.nm, s.pq1, s.pq2, s.pq3, s.pq4, s.pyr, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, s.aq1, s.aq2, s.aq3, s.aq4, s.ayr, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, s.fq1, s.fq2, s.fq3, s.fq4, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, s.np2, s.np3, s.np4, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC FROM oper_s s ) SELECT m.row_type, m.depth, m.sc, m.nm, m.pq1, m.pq2, m.pq3, m.pq4, m.pyr, m.d_pq1, m.d_pq2, m.d_pq3, m.d_pq4, m.d_pyr, COALESCE(m.pyr,0) + COALESCE(m.d_pyr,0) AS total_plan_year, m.aq1, m.aq2, m.aq3, m.aq4, m.ayr, m.d_aq1, m.d_aq2, m.d_aq3, m.d_aq4, m.d_ayr, COALESCE(m.ayr,0) + COALESCE(m.d_ayr,0) AS total_appr_year, m.fq1, m.fq2, m.fq3, m.fq4, COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0), m.d_fq1, m.d_fq2, m.d_fq3, m.d_fq4, COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0), COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0) + COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0), m.np2, m.np3, m.np4, m.d_np2, m.d_np3, m.d_np4 FROM merged m ORDER BY m.sc; END; $function$ ; -- DROP FUNCTION v3.v_form2_sheet_sections(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_seq_ssp_q1 numeric, col_seq_ssp_q2 numeric, col_seq_ssp_q3 numeric, col_seq_ssp_q4 numeric, col_seq_ssp_year numeric, col_seq_ssp_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_vat_rate character varying, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, col_pay_year numeric, col_economy_year numeric, _sort_path integer[]) LANGUAGE plpgsql STABLE AS $function$ #variable_conflict use_column DECLARE s_plan BOOL; s_seq_d BOOL; s_seq_s BOOL; s_appr BOOL; s_cd BOOL; s_book BOOL; s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; s_need_ap BOOL; -- plan/seq/reserve нужны для approved + cp1..cp4 BEGIN s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); s_seq_d := p_sections IS NULL OR 'seq_dfip' = ANY(p_sections); s_seq_s := p_sections IS NULL OR 'seq_ssp' = ANY(p_sections); s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); s_cd := p_sections IS NULL OR 'contract' = ANY(p_sections); s_book := p_sections IS NULL OR 'booking' = ANY(p_sections); s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; RETURN QUERY WITH tw AS ( SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids FROM v3.mv_expense_item_tree t JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code WHERE t.sheet = p_sheet ), page AS ( SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.name AS bname, bl.vsp_id FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet ), -- Conditional joins (gated by section flags) jp AS (SELECT p.* FROM v3.plan p JOIN page pg ON p.line_id = pg.lid WHERE s_need_ap), jsd AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='DFIP'), jsg AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='SSP_GO'), jr AS (SELECT r.* FROM v3.reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_need_ap), jcd AS (SELECT c.* FROM v3.contract_detail c JOIN page pg ON c.line_id = pg.lid WHERE s_cd), jck AS (SELECT c.* FROM v3.ckk c JOIN page pg ON c.line_id = pg.lid WHERE s_book), jq1 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q1 AND q.quarter=1), jq2 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q2 AND q.quarter=2), jq3 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q3 AND q.quarter=3), jq4 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE (s_q4 OR s_tot) AND q.quarter=4), -- INPUT строки: enriched данные input_base AS ( SELECT pg.lid, pg.eid, pg.bname, pg.vsp_id, v.address AS vsp_addr, t.parent_item_id AS sc, t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, t.path AS tree_path, -- plan COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, p.comment AS pcmt, -- seq dfip / ssp_go COALESCE(sd.adj_q1,0) AS dq1, COALESCE(sd.adj_q2,0) AS dq2, COALESCE(sd.adj_q3,0) AS dq3, COALESCE(sd.adj_q4,0) AS dq4, sd.justification AS djust, COALESCE(sg.adj_q1,0) AS gq1, COALESCE(sg.adj_q2,0) AS gq2, COALESCE(sg.adj_q3,0) AS gq3, COALESCE(sg.adj_q4,0) AS gq4, sg.justification AS gjust, -- reserve COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, -- contract cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.contract_date AS cd_dt, cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil, cd.vat_rate AS cd_vat, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, cd.act AS cd_act, cd.comment AS cd_cmt, -- ckk (booking) ck.expenses_q1 AS bk1, ck.expenses_q2 AS bk2, ck.expenses_q3 AS bk3, ck.expenses_q4 AS bk4, ck.expenses_next_year_q1 AS bn1, ck.expenses_next_year_q2 AS bn2, ck.expenses_next_year_q3 AS bn3, ck.expenses_next_year_q4 AS bn4, -- Q1 q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt, q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, q1.payment_comment AS q1_pcmt, q1.payment_act AS q1_pact, q1.booking_amount AS q1_book, q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, q1.transfer_to_economy AS q1_te, q1.transfer_delay_acts AS q1_tda, q1.transfer_delay_procurement AS q1_tdp, q1.transfer_economy_rf AS q1_terf, q1.transfer_next_comment AS q1_tnc, q1.transfer_far_comment AS q1_tfc, -- Q2 q2.target_change AS q2_tc, q2.base_plan_correction AS q2_bc, q2.base_plan_correction_comment AS q2_bcc, q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, q2.plan_revision_comment AS q2_rcmt, q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_reserve AS q2_arv, q2.adj_comment AS q2_acmt, q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, q2.booking_amount AS q2_book, q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, q2.transfer_to_economy AS q2_te, q2.transfer_delay_acts AS q2_tda, q2.transfer_delay_procurement AS q2_tdp, q2.transfer_economy_rf AS q2_terf, q2.transfer_next_comment AS q2_tnc, q2.transfer_far_comment AS q2_tfc, -- Q3 q3.target_change AS q3_tc, q3.base_plan_correction AS q3_bc, q3.base_plan_correction_comment AS q3_bcc, q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, q3.plan_revision_comment AS q3_rcmt, q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_reserve AS q3_arv, q3.adj_comment AS q3_acmt, q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, q3.booking_amount AS q3_book, q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, q3.transfer_to_q4 AS q3_tq4, q3.transfer_to_economy AS q3_te, q3.transfer_delay_acts AS q3_tda, q3.transfer_delay_procurement AS q3_tdp, q3.transfer_economy_rf AS q3_terf, q3.transfer_next_comment AS q3_tnc, -- Q4 q4.target_change AS q4_tc, q4.base_plan_correction AS q4_bc, q4.base_plan_correction_comment AS q4_bcc, q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, q4.plan_revision_comment AS q4_rcmt, q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_reserve AS q4_arv, q4.adj_comment AS q4_acmt, q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, q4.booking_amount AS q4_book, q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod, q4.transfer_to_economy AS q4_te, -- approved per quarter COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(sg.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(sg.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(sg.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(sg.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4 FROM page pg JOIN tw t ON t.id = pg.eid LEFT JOIN v3.vsp v ON v.id = pg.vsp_id LEFT JOIN jp p ON p.line_id = pg.lid LEFT JOIN jsd sd ON sd.line_id = pg.lid LEFT JOIN jsg sg ON sg.line_id = pg.lid LEFT JOIN jr r ON r.line_id = pg.lid LEFT JOIN jcd cd ON cd.line_id = pg.lid LEFT JOIN jck ck ON ck.line_id = pg.lid LEFT JOIN jq1 q1 ON q1.line_id = pg.lid LEFT JOIN jq2 q2 ON q2.line_id = pg.lid LEFT JOIN jq3 q3 ON q3.line_id = pg.lid LEFT JOIN jq4 q4 ON q4.line_id = pg.lid ), -- Computed: corrected/new plans enriched AS ( SELECT b.*, -- Q1 corrected_plan = approved + adj_* b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_arv,0) AS cp1, COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, -- Q2 new_plan = approved + transfer_q1 + revisions(inc/seq) + base_correction b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) + COALESCE(b.q2_bc,0) AS np2, COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) + COALESCE(b.q3_bc,0) AS np3, COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) + COALESCE(b.q4_bc,0) AS np4, COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 FROM input_base b ), final_input AS ( SELECT e.*, e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_arv,0) AS cp2, e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_arv,0) AS cp3, e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_arv,0) AS cp4 FROM enriched e ), -- Aggregates per expense_item (для иерархии) agg AS ( SELECT bl.expense_item_id AS eid, SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, SUM(sd.adj_q1) AS sd1, SUM(sd.adj_q2) AS sd2, SUM(sd.adj_q3) AS sd3, SUM(sd.adj_q4) AS sd4, SUM(sg.adj_q1) AS sg1, SUM(sg.adj_q2) AS sg2, SUM(sg.adj_q3) AS sg3, SUM(sg.adj_q4) AS sg4, SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, SUM(ck.expenses_q1) AS bk1, SUM(ck.expenses_q2) AS bk2, SUM(ck.expenses_q3) AS bk3, SUM(ck.expenses_q4) AS bk4, SUM(ck.expenses_next_year_q1) AS bn1, SUM(ck.expenses_next_year_q2) AS bn2, SUM(ck.expenses_next_year_q3) AS bn3, SUM(ck.expenses_next_year_q4) AS bn4 FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN v3.plan p ON p.line_id = bl.id AND s_need_ap LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor='DFIP' AND s_need_ap LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_need_ap OR s_book) GROUP BY bl.expense_item_id ), -- Per-quarter aggregates from blq aq_q AS ( SELECT bl.expense_item_id AS eid, q.quarter, SUM(q.adj_current) AS ac, SUM(q.adj_ssp) AS as_v, SUM(q.adj_reserve) AS arv, SUM(q.payment_amount) AS pa, SUM(q.booking_amount) AS bk, SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, SUM(q.actual_spod) AS spod, SUM(q.transfer_to_q2) AS tq2, SUM(q.transfer_to_q3) AS tq3, SUM(q.transfer_to_q4) AS tq4, SUM(q.transfer_to_economy) AS te, SUM(q.transfer_delay_acts) AS tda, SUM(q.transfer_delay_procurement) AS tdp, SUM(q.transfer_economy_rf) AS terf, -- Δ к approved для перехода к new_plan: revision_inc/seq + target_change + base_correction -- (Excel: DJ10 = SUM(AN, CY, DH, CF, DE:DG)). Должно совпадать с INPUT-формулой np. SUM(COALESCE(q.plan_revision_increase,0) + COALESCE(q.plan_revision_sequester,0) + COALESCE(q.target_change,0) + COALESCE(q.base_plan_correction,0)) AS rev FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id JOIN v3.budget_line_quarter q ON q.line_id = bl.id WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet GROUP BY bl.expense_item_id, q.quarter ), -- Tree-rollup tw_agg AS ( SELECT tw.id, SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, SUM(a.sd1) AS sd1, SUM(a.sd2) AS sd2, SUM(a.sd3) AS sd3, SUM(a.sd4) AS sd4, SUM(a.sg1) AS sg1, SUM(a.sg2) AS sg2, SUM(a.sg3) AS sg3, SUM(a.sg4) AS sg4, SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, SUM(a.bk1) AS bk1, SUM(a.bk2) AS bk2, SUM(a.bk3) AS bk3, SUM(a.bk4) AS bk4, SUM(a.bn1) AS bn1, SUM(a.bn2) AS bn2, SUM(a.bn3) AS bn3, SUM(a.bn4) AS bn4 FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) GROUP BY tw.id ), tw_aq1 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.bk) AS bk, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=1 GROUP BY tw.id ), tw_aq2 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.bk) AS bk, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev, SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=2 GROUP BY tw.id ), tw_aq3 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.bk) AS bk, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev, SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=3 GROUP BY tw.id ), tw_aq4 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.bk) AS bk, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod, SUM(b.te) AS te, SUM(b.rev) AS rev FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=4 GROUP BY tw.id ) -- ═══ Часть A: INPUT строки ═════════════════════════════════════════════ SELECT * FROM ( SELECT 'INPUT'::VARCHAR, 3, f.lid::INT, f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.vsp_id, f.vsp_addr, -- plan CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, -- seq dfip CASE WHEN s_seq_d THEN f.dq1 END, CASE WHEN s_seq_d THEN f.dq2 END, CASE WHEN s_seq_d THEN f.dq3 END, CASE WHEN s_seq_d THEN f.dq4 END, CASE WHEN s_seq_d THEN f.dq1+f.dq2+f.dq3+f.dq4 END, CASE WHEN s_seq_d THEN f.djust END, -- seq ssp_go CASE WHEN s_seq_s THEN f.gq1 END, CASE WHEN s_seq_s THEN f.gq2 END, CASE WHEN s_seq_s THEN f.gq3 END, CASE WHEN s_seq_s THEN f.gq4 END, CASE WHEN s_seq_s THEN f.gq1+f.gq2+f.gq3+f.gq4 END, CASE WHEN s_seq_s THEN f.gjust END, -- approved CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, -- contract CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, CASE WHEN s_cd THEN f.cd_dt END, CASE WHEN s_cd THEN f.cd_subj END, CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, CASE WHEN s_cd THEN f.cd_vat END, CASE WHEN s_cd THEN f.cd_dl END, CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END, CASE WHEN s_cd THEN f.cd_cmt END, -- booking 2026 CASE WHEN s_book THEN f.bk1 END, CASE WHEN s_book THEN f.bk2 END, CASE WHEN s_book THEN f.bk3 END, CASE WHEN s_book THEN f.bk4 END, -- booking 2027 CASE WHEN s_book THEN f.bn1 END, CASE WHEN s_book THEN f.bn2 END, CASE WHEN s_book THEN f.bn3 END, CASE WHEN s_book THEN f.bn4 END, -- Q1 CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, CASE WHEN s_q1 THEN f.q1_arv END, CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, CASE WHEN s_q1 THEN f.q1_pcmt END, CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN f.q1_book END, CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, CASE WHEN s_q1 THEN f.aq1 END, CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.q1_book,0) END, -- residual_after_booking CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, -- residual_after_actual CASE WHEN s_q1 THEN f.q1_tq2 END, CASE WHEN s_q1 THEN f.q1_tda END, CASE WHEN s_q1 THEN f.q1_tdp END, CASE WHEN s_q1 THEN f.q1_terf END, CASE WHEN s_q1 THEN f.q1_tnc END, CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, CASE WHEN s_q1 THEN f.q1_tfc END, CASE WHEN s_q1 THEN f.q1_te END, -- total = SUM transfer_q2_reasons + transfer_q3 + transfer_q4 + transfer_econ (Excel CC5) CASE WHEN s_q1 THEN COALESCE(f.q1_tda,0)+COALESCE(f.q1_tdp,0)+COALESCE(f.q1_terf,0) +COALESCE(f.q1_tq3,0)+COALESCE(f.q1_tq4,0)+COALESCE(f.q1_te,0) END, -- Q2 CASE WHEN s_q2 THEN f.q2_tc END, CASE WHEN s_q2 THEN f.q2_bc END, CASE WHEN s_q2 THEN f.q2_bcc END, CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, CASE WHEN s_q2 THEN f.q2_rcmt END, CASE WHEN s_q2 THEN f.np2 END, CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, CASE WHEN s_q2 THEN f.q2_arv END, CASE WHEN s_q2 THEN f.q2_acmt END, CASE WHEN s_q2 THEN f.cp2 END, CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, CASE WHEN s_q2 THEN f.q2_book END, CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, CASE WHEN s_q2 THEN f.aq2 END, CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.q2_book,0) END, CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, CASE WHEN s_q2 THEN f.q2_tq3 END, CASE WHEN s_q2 THEN f.q2_tda END, CASE WHEN s_q2 THEN f.q2_tdp END, CASE WHEN s_q2 THEN f.q2_terf END, CASE WHEN s_q2 THEN f.q2_tnc END, CASE WHEN s_q2 THEN f.q2_tq4 END, CASE WHEN s_q2 THEN f.q2_tfc END, CASE WHEN s_q2 THEN f.q2_te END, -- total = SUM transfer_q3_reasons + transfer_q4 + transfer_econ (Excel DN5) CASE WHEN s_q2 THEN COALESCE(f.q2_tda,0)+COALESCE(f.q2_tdp,0)+COALESCE(f.q2_terf,0) +COALESCE(f.q2_tq4,0)+COALESCE(f.q2_te,0) END, -- Q3 CASE WHEN s_q3 THEN f.q3_tc END, CASE WHEN s_q3 THEN f.q3_bc END, CASE WHEN s_q3 THEN f.q3_bcc END, CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, CASE WHEN s_q3 THEN f.q3_rcmt END, CASE WHEN s_q3 THEN f.np3 END, CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, CASE WHEN s_q3 THEN f.q3_arv END, CASE WHEN s_q3 THEN f.q3_acmt END, CASE WHEN s_q3 THEN f.cp3 END, CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, CASE WHEN s_q3 THEN f.q3_book END, CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, CASE WHEN s_q3 THEN f.aq3 END, CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.q3_book,0) END, CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, CASE WHEN s_q3 THEN f.q3_tq4 END, CASE WHEN s_q3 THEN f.q3_tda END, CASE WHEN s_q3 THEN f.q3_tdp END, CASE WHEN s_q3 THEN f.q3_terf END, CASE WHEN s_q3 THEN f.q3_tnc END, CASE WHEN s_q3 THEN f.q3_te END, -- total = SUM transfer_q4_reasons + transfer_econ (Excel EW5) CASE WHEN s_q3 THEN COALESCE(f.q3_tda,0)+COALESCE(f.q3_tdp,0)+COALESCE(f.q3_terf,0) +COALESCE(f.q3_te,0) END, -- Q4 CASE WHEN s_q4 THEN f.q4_tc END, CASE WHEN s_q4 THEN f.q4_bc END, CASE WHEN s_q4 THEN f.q4_bcc END, CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, CASE WHEN s_q4 THEN f.q4_rcmt END, CASE WHEN s_q4 THEN f.np4 END, CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, CASE WHEN s_q4 THEN f.q4_arv END, CASE WHEN s_q4 THEN f.q4_acmt END, CASE WHEN s_q4 THEN f.cp4 END, CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, CASE WHEN s_q4 THEN f.q4_book END, CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, CASE WHEN s_q4 THEN f.q4_spod END, CASE WHEN s_q4 THEN f.aq4 END, CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.q4_book,0) END, CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, CASE WHEN s_q4 THEN f.q4_te END, -- q4.total = q4.transfer_econ (Q4 has no other outgoing transfers) CASE WHEN s_q4 THEN f.q4_te END, -- totals CASE WHEN s_tot THEN f.aq1+f.aq2+f.aq3+f.aq4 END, CASE WHEN s_tot THEN COALESCE(f.q1_pa,0)+COALESCE(f.q2_pa,0)+COALESCE(f.q3_pa,0)+COALESCE(f.q4_pa,0) END, -- economy_year = q4.corrected_plan − q4.actual_quarter (Excel GA5 = FY5 = FK5 − FW5) CASE WHEN s_tot THEN f.cp4 - f.aq4 END, f.tree_path || ARRAY[f.lid::INT] AS _sort FROM final_input f UNION ALL -- ═══ Часть B: Иерархия ════════════════════════════════════════════════ SELECT CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR, t.depth, NULL::INT, CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END, CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END, t.num_group_id, t.name, NULL::INT, NULL::VARCHAR, -- plan CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END, CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END, CASE WHEN s_plan THEN COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) END, NULL::VARCHAR, -- seq dfip CASE WHEN s_seq_d THEN ta.sd1 END, CASE WHEN s_seq_d THEN ta.sd2 END, CASE WHEN s_seq_d THEN ta.sd3 END, CASE WHEN s_seq_d THEN ta.sd4 END, CASE WHEN s_seq_d THEN COALESCE(ta.sd1,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sd4,0) END, NULL::VARCHAR, -- seq ssp_go CASE WHEN s_seq_s THEN ta.sg1 END, CASE WHEN s_seq_s THEN ta.sg2 END, CASE WHEN s_seq_s THEN ta.sg3 END, CASE WHEN s_seq_s THEN ta.sg4 END, CASE WHEN s_seq_s THEN COALESCE(ta.sg1,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sg4,0) END, NULL::VARCHAR, -- approved CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0) +COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0) +COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0) +COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END, -- contract — нет агрегации (текстовые) NULL::VARCHAR, NULL::VARCHAR, NULL::DATE, NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- booking 2026 / 2027 CASE WHEN s_book THEN ta.bk1 END, CASE WHEN s_book THEN ta.bk2 END, CASE WHEN s_book THEN ta.bk3 END, CASE WHEN s_book THEN ta.bk4 END, CASE WHEN s_book THEN ta.bn1 END, CASE WHEN s_book THEN ta.bn2 END, CASE WHEN s_book THEN ta.bn3 END, CASE WHEN s_book THEN ta.bn4 END, -- Q1 hierarchy: новые поля (pay_act, booking, residuals, transfer reasons, -- transfer comments) на агрегатах NULL'им — их корректный rollup можно -- добавить позже расширением aq_q. CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as_v END, CASE WHEN s_q1 THEN tb1.arv END, NULL::VARCHAR, CASE WHEN s_q1 THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0) END, NULL::DATE, CASE WHEN s_q1 THEN tb1.pa END, NULL::VARCHAR, -- pay_comment NULL::VARCHAR, -- pay_act CASE WHEN s_q1 THEN tb1.bk END, -- booking (rollup) CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END, CASE WHEN s_q1 THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) END, -- residual_after_booking = q1.cp − Σ booking CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0)) - COALESCE(tb1.bk,0) END, -- residual_after_actual = q1.cp − Σ actual CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0)) - (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) END, CASE WHEN s_q1 THEN tb1.tq2 END, CASE WHEN s_q1 THEN tb1.tda END, CASE WHEN s_q1 THEN tb1.tdp END, CASE WHEN s_q1 THEN tb1.terf END, NULL::VARCHAR, -- transfer_next_comment (text, no rollup) CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END, NULL::VARCHAR, -- far comment CASE WHEN s_q1 THEN tb1.te END, -- total = SUM transfer_q2_reasons + transfer_q3 + transfer_q4 + transfer_econ (Excel CC) CASE WHEN s_q1 THEN COALESCE(tb1.tda,0)+COALESCE(tb1.tdp,0)+COALESCE(tb1.terf,0) +COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)+COALESCE(tb1.te,0) END, -- Q2 hierarchy NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq NULL::VARCHAR, -- revision_comment CASE WHEN s_q2 THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) END, CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as_v END, CASE WHEN s_q2 THEN tb2.arv END, NULL::VARCHAR, -- adj_comment CASE WHEN s_q2 THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0) END, NULL::DATE, CASE WHEN s_q2 THEN tb2.pa END, NULL::VARCHAR, -- pay_comment NULL::VARCHAR, -- pay_act CASE WHEN s_q2 THEN tb2.bk END, -- booking (rollup) CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END, CASE WHEN s_q2 THEN COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) END, -- residual_after_booking = q2.cp − Σ booking CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0)) - COALESCE(tb2.bk,0) END, -- residual_after_actual = q2.cp − Σ actual CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0)) - (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) END, CASE WHEN s_q2 THEN tb2.tq3 END, CASE WHEN s_q2 THEN tb2.tda END, CASE WHEN s_q2 THEN tb2.tdp END, CASE WHEN s_q2 THEN tb2.terf END, NULL::VARCHAR, -- transfer_next_comment (text, no rollup) CASE WHEN s_q2 THEN tb2.tq4 END, NULL::VARCHAR, -- far comment CASE WHEN s_q2 THEN tb2.te END, -- total = SUM transfer_q3_reasons + transfer_q4 + transfer_econ (Excel DN) CASE WHEN s_q2 THEN COALESCE(tb2.tda,0)+COALESCE(tb2.tdp,0)+COALESCE(tb2.terf,0) +COALESCE(tb2.tq4,0)+COALESCE(tb2.te,0) END, -- Q3 hierarchy NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq NULL::VARCHAR, CASE WHEN s_q3 THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) END, CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as_v END, CASE WHEN s_q3 THEN tb3.arv END, NULL::VARCHAR, CASE WHEN s_q3 THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0) END, NULL::DATE, CASE WHEN s_q3 THEN tb3.pa END, NULL::VARCHAR, -- pay_comment NULL::VARCHAR, -- pay_act CASE WHEN s_q3 THEN tb3.bk END, -- booking (rollup) CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END, CASE WHEN s_q3 THEN COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) END, -- residual_after_booking = q3.cp − Σ booking CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0)) - COALESCE(tb3.bk,0) END, -- residual_after_actual = q3.cp − Σ actual CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0)) - (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) END, CASE WHEN s_q3 THEN tb3.tq4 END, CASE WHEN s_q3 THEN tb3.tda END, CASE WHEN s_q3 THEN tb3.tdp END, CASE WHEN s_q3 THEN tb3.terf END, NULL::VARCHAR, -- transfer_next_comment (text, no rollup) CASE WHEN s_q3 THEN tb3.te END, -- total = SUM transfer_q4_reasons + transfer_econ (Excel EW) CASE WHEN s_q3 THEN COALESCE(tb3.tda,0)+COALESCE(tb3.tdp,0)+COALESCE(tb3.terf,0) +COALESCE(tb3.te,0) END, -- Q4 hierarchy NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq NULL::VARCHAR, CASE WHEN s_q4 THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) END, CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as_v END, CASE WHEN s_q4 THEN tb4.arv END, NULL::VARCHAR, CASE WHEN s_q4 THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0) END, NULL::DATE, CASE WHEN s_q4 THEN tb4.pa END, NULL::VARCHAR, -- pay_comment NULL::VARCHAR, -- pay_act CASE WHEN s_q4 THEN tb4.bk END, -- booking (rollup) CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END, CASE WHEN s_q4 THEN tb4.spod END, CASE WHEN s_q4 THEN COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, -- residual_after_booking = q4.cp − Σ booking CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) - COALESCE(tb4.bk,0) END, -- residual_after_actual = q4.cp − Σ actual (incl. spod) CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) END, CASE WHEN s_q4 THEN tb4.te END, -- q4.total = q4.transfer_econ (Q4 has no other outgoing transfers) CASE WHEN s_q4 THEN tb4.te END, -- totals CASE WHEN s_tot THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) +COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) +COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) +COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, CASE WHEN s_tot THEN COALESCE(tb1.pa,0)+COALESCE(tb2.pa,0)+COALESCE(tb3.pa,0)+COALESCE(tb4.pa,0) END, -- economy_year = q4.corrected_plan − q4.actual_quarter (Excel GA = FY = FK − FW) CASE WHEN s_tot THEN (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) END, t.path AS _sort FROM tw t LEFT JOIN tw_agg ta ON ta.id = t.id LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id ) sub ORDER BY sub._sort; END; $function$ ; -- DROP FUNCTION v3.v_form2_sheet_jsonb(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form2_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, seq_ssp_data jsonb, approved_data jsonb, contract_data jsonb, booking_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[]) LANGUAGE sql STABLE AS $function$ WITH flags AS ( SELECT (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, (p_sections IS NULL OR 'seq_dfip' = ANY(p_sections)) AS s_seq_d, (p_sections IS NULL OR 'seq_ssp' = ANY(p_sections)) AS s_seq_s, (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, (p_sections IS NULL OR 'contract' = ANY(p_sections)) AS s_cd, (p_sections IS NULL OR 'booking' = ANY(p_sections)) AS s_book, (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot ) SELECT s.row_type, s.depth, s.line_id, -- header (всегда). expense_item_id из _sort_path (см. v_form1_jsonb). jsonb_build_object( 'section_code', s.col_section_code, 'item_id', s.col_item_id, 'num_group', s.col_num_group_id, 'name', s.col_name, 'vsp_id', s.col_vsp_id, 'vsp_address', s.col_vsp_address, 'expense_item_id', CASE WHEN s.row_type = 'INPUT' THEN s._sort_path[array_upper(s._sort_path,1)-1] ELSE s._sort_path[array_upper(s._sort_path,1)] END ) AS header, CASE WHEN f.s_plan THEN jsonb_build_object( 'q1', s.col_plan_q1, 'q2', s.col_plan_q2, 'q3', s.col_plan_q3, 'q4', s.col_plan_q4, 'year', s.col_plan_year, 'comment', s.col_plan_comment ) END AS plan_data, CASE WHEN f.s_seq_d THEN jsonb_build_object( 'q1', s.col_seq_dfip_q1, 'q2', s.col_seq_dfip_q2, 'q3', s.col_seq_dfip_q3, 'q4', s.col_seq_dfip_q4, 'year', s.col_seq_dfip_year, 'justification', s.col_seq_dfip_just ) END AS seq_dfip_data, CASE WHEN f.s_seq_s THEN jsonb_build_object( 'q1', s.col_seq_ssp_q1, 'q2', s.col_seq_ssp_q2, 'q3', s.col_seq_ssp_q3, 'q4', s.col_seq_ssp_q4, 'year', s.col_seq_ssp_year, 'justification', s.col_seq_ssp_just ) END AS seq_ssp_data, CASE WHEN f.s_appr THEN jsonb_build_object( 'q1', s.col_appr_q1, 'q2', s.col_appr_q2, 'q3', s.col_appr_q3, 'q4', s.col_appr_q4, 'year', s.col_appr_year ) END AS approved_data, CASE WHEN f.s_cd THEN jsonb_build_object( 'counterparty', s.col_cd_counterparty, 'reference', s.col_cd_reference, 'date', s.col_cd_date, 'subject', s.col_cd_subject, 'currency', s.col_cd_currency, 'ceiling', s.col_cd_ceiling, 'vat_rate', s.col_cd_vat_rate, 'deadline', s.col_cd_deadline, 'scheme', s.col_cd_scheme, 'act', s.col_cd_act, 'comment', s.col_cd_comment ) END AS contract_data, CASE WHEN f.s_book THEN jsonb_build_object( 'y2026', jsonb_build_object('q1', s.col_book_q1, 'q2', s.col_book_q2, 'q3', s.col_book_q3, 'q4', s.col_book_q4), 'y2027', jsonb_build_object('q1', s.col_book_next_q1, 'q2', s.col_book_next_q2, 'q3', s.col_book_next_q3, 'q4', s.col_book_next_q4) ) END AS booking_data, CASE WHEN f.s_q1 THEN jsonb_build_object( 'adj_current', s.col_q1_adj_current, 'adj_ssp', s.col_q1_adj_ssp, 'adj_reserve', s.col_q1_adj_reserve, 'adj_comment', s.col_q1_adj_comment, 'corrected_plan', s.col_q1_corrected_plan, 'pay_date', s.col_q1_pay_date, 'pay_amount', s.col_q1_pay_amount, 'pay_comment', s.col_q1_pay_comment, 'pay_act', s.col_q1_pay_act, 'booking', s.col_q1_booking, 'actual_m1', s.col_q1_actual_m1, 'actual_m2', s.col_q1_actual_m2, 'actual_m3', s.col_q1_actual_m3, 'actual_quarter', s.col_q1_actual_quarter, 'residual_after_booking', s.col_q1_residual_after_booking, 'residual_after_actual', s.col_q1_residual_after_actual, 'transfer_q2', s.col_q1_transfer_q2, 'transfer_q2_delay_acts', s.col_q1_transfer_q2_delay_acts, 'transfer_q2_delay_procurement', s.col_q1_transfer_q2_delay_procurement, 'transfer_q2_economy_rf', s.col_q1_transfer_q2_economy_rf, 'transfer_next_comment', s.col_q1_transfer_next_comment, 'transfer_q3', s.col_q1_transfer_q3, 'transfer_q4', s.col_q1_transfer_q4, 'transfer_far_comment', s.col_q1_transfer_far_comment, 'transfer_econ', s.col_q1_transfer_econ, 'total', s.col_q1_total ) END AS q1_data, CASE WHEN f.s_q2 THEN jsonb_build_object( 'target_change', s.col_q2_target_change, 'base_correction', s.col_q2_base_correction, 'base_correction_comment', s.col_q2_base_correction_comment, 'revision_inc', s.col_q2_revision_inc, 'revision_seq', s.col_q2_revision_seq, 'revision_comment', s.col_q2_revision_comment, 'new_plan', s.col_q2_new_plan, 'adj_current', s.col_q2_adj_current, 'adj_ssp', s.col_q2_adj_ssp, 'adj_reserve', s.col_q2_adj_reserve, 'adj_comment', s.col_q2_adj_comment, 'corrected_plan', s.col_q2_corrected_plan, 'pay_date', s.col_q2_pay_date, 'pay_amount', s.col_q2_pay_amount, 'pay_comment', s.col_q2_pay_comment, 'pay_act', s.col_q2_pay_act, 'booking', s.col_q2_booking, 'actual_m1', s.col_q2_actual_m1, 'actual_m2', s.col_q2_actual_m2, 'actual_m3', s.col_q2_actual_m3, 'actual_quarter', s.col_q2_actual_quarter, 'residual_after_booking', s.col_q2_residual_after_booking, 'residual_after_actual', s.col_q2_residual_after_actual, 'transfer_q3', s.col_q2_transfer_q3, 'transfer_q3_delay_acts', s.col_q2_transfer_q3_delay_acts, 'transfer_q3_delay_procurement', s.col_q2_transfer_q3_delay_procurement, 'transfer_q3_economy_rf', s.col_q2_transfer_q3_economy_rf, 'transfer_next_comment', s.col_q2_transfer_next_comment, 'transfer_q4', s.col_q2_transfer_q4, 'transfer_far_comment', s.col_q2_transfer_far_comment, 'transfer_econ', s.col_q2_transfer_econ, 'total', s.col_q2_total ) END AS q2_data, CASE WHEN f.s_q3 THEN jsonb_build_object( 'target_change', s.col_q3_target_change, 'base_correction', s.col_q3_base_correction, 'base_correction_comment', s.col_q3_base_correction_comment, 'revision_inc', s.col_q3_revision_inc, 'revision_seq', s.col_q3_revision_seq, 'revision_comment', s.col_q3_revision_comment, 'new_plan', s.col_q3_new_plan, 'adj_current', s.col_q3_adj_current, 'adj_ssp', s.col_q3_adj_ssp, 'adj_reserve', s.col_q3_adj_reserve, 'adj_comment', s.col_q3_adj_comment, 'corrected_plan', s.col_q3_corrected_plan, 'pay_date', s.col_q3_pay_date, 'pay_amount', s.col_q3_pay_amount, 'pay_comment', s.col_q3_pay_comment, 'pay_act', s.col_q3_pay_act, 'booking', s.col_q3_booking, 'actual_m1', s.col_q3_actual_m1, 'actual_m2', s.col_q3_actual_m2, 'actual_m3', s.col_q3_actual_m3, 'actual_quarter', s.col_q3_actual_quarter, 'residual_after_booking', s.col_q3_residual_after_booking, 'residual_after_actual', s.col_q3_residual_after_actual, 'transfer_q4', s.col_q3_transfer_q4, 'transfer_q4_delay_acts', s.col_q3_transfer_q4_delay_acts, 'transfer_q4_delay_procurement', s.col_q3_transfer_q4_delay_procurement, 'transfer_q4_economy_rf', s.col_q3_transfer_q4_economy_rf, 'transfer_next_comment', s.col_q3_transfer_next_comment, 'transfer_econ', s.col_q3_transfer_econ, 'total', s.col_q3_total ) END AS q3_data, CASE WHEN f.s_q4 THEN jsonb_build_object( 'target_change', s.col_q4_target_change, 'base_correction', s.col_q4_base_correction, 'base_correction_comment', s.col_q4_base_correction_comment, 'revision_inc', s.col_q4_revision_inc, 'revision_seq', s.col_q4_revision_seq, 'revision_comment', s.col_q4_revision_comment, 'new_plan', s.col_q4_new_plan, 'adj_current', s.col_q4_adj_current, 'adj_ssp', s.col_q4_adj_ssp, 'adj_reserve', s.col_q4_adj_reserve, 'adj_comment', s.col_q4_adj_comment, 'corrected_plan', s.col_q4_corrected_plan, 'pay_date', s.col_q4_pay_date, 'pay_amount', s.col_q4_pay_amount, 'pay_comment', s.col_q4_pay_comment, 'pay_act', s.col_q4_pay_act, 'booking', s.col_q4_booking, 'actual_m1', s.col_q4_actual_m1, 'actual_m2', s.col_q4_actual_m2, 'actual_m3', s.col_q4_actual_m3, 'actual_spod', s.col_q4_actual_spod, 'actual_quarter', s.col_q4_actual_quarter, 'residual_after_booking', s.col_q4_residual_after_booking, 'residual_after_actual', s.col_q4_residual_after_actual, 'transfer_econ', s.col_q4_transfer_econ, 'total', s.col_q4_total ) END AS q4_data, CASE WHEN f.s_tot THEN jsonb_build_object( 'fact_year', s.col_fact_year, 'pay_year', s.col_pay_year, 'economy_year', s.col_economy_year ) END AS totals_data, s._sort_path FROM v3.v_form2_sheet_sections(p_form_id, p_sheet, p_sections) s CROSS JOIN flags f ORDER BY s._sort_path; $function$ ; -- DROP FUNCTION v3.v_form2_smeta(int4); CREATE OR REPLACE FUNCTION v3.v_form2_smeta(p_form_id integer) RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, plan_supp_q1 numeric, plan_supp_q2 numeric, plan_supp_q3 numeric, plan_supp_q4 numeric, plan_supp_year numeric, plan_dev_q1 numeric, plan_dev_q2 numeric, plan_dev_q3 numeric, plan_dev_q4 numeric, plan_dev_year numeric, appr_supp_q1 numeric, appr_supp_q2 numeric, appr_supp_q3 numeric, appr_supp_q4 numeric, appr_supp_year numeric, appr_dev_q1 numeric, appr_dev_q2 numeric, appr_dev_q3 numeric, appr_dev_q4 numeric, appr_dev_year numeric, fact_supp_q1 numeric, fact_supp_q2 numeric, fact_supp_q3 numeric, fact_supp_q4 numeric, fact_supp_year numeric, fact_dev_q1 numeric, fact_dev_q2 numeric, fact_dev_q3 numeric, fact_dev_q4 numeric, fact_dev_year numeric, corr_supp_q2 numeric, corr_supp_q3 numeric, corr_supp_q4 numeric, corr_dev_q2 numeric, corr_dev_q3 numeric, corr_dev_q4 numeric) LANGUAGE plpgsql STABLE AS $function$ #variable_conflict use_column BEGIN RETURN QUERY WITH -- Per-line per-direction агрегаты квартальных данных q_per_line AS ( SELECT line_id, SUM(CASE WHEN quarter=1 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq1, SUM(CASE WHEN quarter=2 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq2, SUM(CASE WHEN quarter=3 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq3, SUM(CASE WHEN quarter=4 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0)+COALESCE(actual_spod,0) END) AS fq4, SUM(CASE WHEN quarter=1 THEN transfer_to_q2 END) AS t1q2, SUM(CASE WHEN quarter=1 THEN transfer_to_q3 END) AS t1q3, SUM(CASE WHEN quarter=1 THEN transfer_to_q4 END) AS t1q4, SUM(CASE WHEN quarter=2 THEN transfer_to_q3 END) AS t2q3, SUM(CASE WHEN quarter=2 THEN transfer_to_q4 END) AS t2q4, SUM(CASE WHEN quarter=3 THEN transfer_to_q4 END) AS t3q4, SUM(CASE WHEN quarter=1 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj1, SUM(CASE WHEN quarter=2 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj2, SUM(CASE WHEN quarter=3 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj3, SUM(CASE WHEN quarter=4 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj4, SUM(CASE WHEN quarter=2 THEN COALESCE(plan_revision_eco_change,0)+COALESCE(plan_revision_item_adj,0)+COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0) END) AS rev2, SUM(CASE WHEN quarter=3 THEN COALESCE(plan_revision_eco_change,0)+COALESCE(plan_revision_item_adj,0)+COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0) END) AS rev3, SUM(CASE WHEN quarter=4 THEN COALESCE(plan_revision_eco_change,0)+COALESCE(plan_revision_item_adj,0)+COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0) END) AS rev4 FROM v3.budget_line_quarter GROUP BY line_id ), seq_dfip AS ( SELECT line_id, SUM(adj_q1) AS s1, SUM(adj_q2) AS s2, SUM(adj_q3) AS s3, SUM(adj_q4) AS s4 FROM v3.sequestration WHERE actor='DFIP' GROUP BY line_id ), seq_ssp AS ( SELECT line_id, SUM(adj_q1) AS g1, SUM(adj_q2) AS g2, SUM(adj_q3) AS g3, SUM(adj_q4) AS g4 FROM v3.sequestration WHERE actor='SSP_GO' GROUP BY line_id ), -- Per-line aggregate: учёт direction из expense_item line_agg AS ( SELECT bl.expense_item_id AS eid, ei.direction AS dir, SUM(p.plan_q1) AS pq1, SUM(p.plan_q2) AS pq2, SUM(p.plan_q3) AS pq3, SUM(p.plan_q4) AS pq4, SUM(sd.s1) AS sd1, SUM(sd.s2) AS sd2, SUM(sd.s3) AS sd3, SUM(sd.s4) AS sd4, SUM(sg.g1) AS sg1, SUM(sg.g2) AS sg2, SUM(sg.g3) AS sg3, SUM(sg.g4) AS sg4, SUM(r.amount_q1) AS rq1, SUM(r.amount_q2) AS rq2, SUM(r.amount_q3) AS rq3, SUM(r.amount_q4) AS rq4, SUM(qp.fq1) AS fq1, SUM(qp.fq2) AS fq2, SUM(qp.fq3) AS fq3, SUM(qp.fq4) AS fq4, SUM(qp.t1q2) AS t1q2, SUM(qp.t1q3) AS t1q3, SUM(qp.t1q4) AS t1q4, SUM(qp.t2q3) AS t2q3, SUM(qp.t2q4) AS t2q4, SUM(qp.t3q4) AS t3q4, SUM(qp.adj1) AS adj1, SUM(qp.adj2) AS adj2, SUM(qp.adj3) AS adj3, SUM(qp.adj4) AS adj4, SUM(qp.rev2) AS rev2, SUM(qp.rev3) AS rev3, SUM(qp.rev4) AS rev4 FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN v3.plan p ON p.line_id = bl.id LEFT JOIN seq_dfip sd ON sd.line_id = bl.id LEFT JOIN seq_ssp sg ON sg.line_id = bl.id LEFT JOIN v3.reserve r ON r.line_id = bl.id LEFT JOIN q_per_line qp ON qp.line_id = bl.id WHERE bl.budget_form_id = p_form_id GROUP BY bl.expense_item_id, ei.direction ), -- Tree rollup с расщеплением по direction. Используем FILTER (WHERE) tw_rollup AS ( SELECT t.id, t.depth, t.item_id AS section_code, t.name, -- Поддержка SUM(a.pq1) FILTER (WHERE a.dir='Support') AS s_pq1, SUM(a.pq2) FILTER (WHERE a.dir='Support') AS s_pq2, SUM(a.pq3) FILTER (WHERE a.dir='Support') AS s_pq3, SUM(a.pq4) FILTER (WHERE a.dir='Support') AS s_pq4, SUM(a.sd1) FILTER (WHERE a.dir='Support') AS s_sd1, SUM(a.sd2) FILTER (WHERE a.dir='Support') AS s_sd2, SUM(a.sd3) FILTER (WHERE a.dir='Support') AS s_sd3, SUM(a.sd4) FILTER (WHERE a.dir='Support') AS s_sd4, SUM(a.sg1) FILTER (WHERE a.dir='Support') AS s_sg1, SUM(a.sg2) FILTER (WHERE a.dir='Support') AS s_sg2, SUM(a.sg3) FILTER (WHERE a.dir='Support') AS s_sg3, SUM(a.sg4) FILTER (WHERE a.dir='Support') AS s_sg4, SUM(a.rq1) FILTER (WHERE a.dir='Support') AS s_rq1, SUM(a.rq2) FILTER (WHERE a.dir='Support') AS s_rq2, SUM(a.rq3) FILTER (WHERE a.dir='Support') AS s_rq3, SUM(a.rq4) FILTER (WHERE a.dir='Support') AS s_rq4, SUM(a.fq1) FILTER (WHERE a.dir='Support') AS s_fq1, SUM(a.fq2) FILTER (WHERE a.dir='Support') AS s_fq2, SUM(a.fq3) FILTER (WHERE a.dir='Support') AS s_fq3, SUM(a.fq4) FILTER (WHERE a.dir='Support') AS s_fq4, SUM(a.t1q2) FILTER (WHERE a.dir='Support') AS s_t1q2, SUM(a.t1q3) FILTER (WHERE a.dir='Support') AS s_t1q3, SUM(a.t1q4) FILTER (WHERE a.dir='Support') AS s_t1q4, SUM(a.t2q3) FILTER (WHERE a.dir='Support') AS s_t2q3, SUM(a.t2q4) FILTER (WHERE a.dir='Support') AS s_t2q4, SUM(a.t3q4) FILTER (WHERE a.dir='Support') AS s_t3q4, SUM(a.adj1) FILTER (WHERE a.dir='Support') AS s_adj1, SUM(a.adj2) FILTER (WHERE a.dir='Support') AS s_adj2, SUM(a.adj3) FILTER (WHERE a.dir='Support') AS s_adj3, SUM(a.adj4) FILTER (WHERE a.dir='Support') AS s_adj4, SUM(a.rev2) FILTER (WHERE a.dir='Support') AS s_rev2, SUM(a.rev3) FILTER (WHERE a.dir='Support') AS s_rev3, SUM(a.rev4) FILTER (WHERE a.dir='Support') AS s_rev4, -- Развитие SUM(a.pq1) FILTER (WHERE a.dir='Development') AS d_pq1, SUM(a.pq2) FILTER (WHERE a.dir='Development') AS d_pq2, SUM(a.pq3) FILTER (WHERE a.dir='Development') AS d_pq3, SUM(a.pq4) FILTER (WHERE a.dir='Development') AS d_pq4, SUM(a.sd1) FILTER (WHERE a.dir='Development') AS d_sd1, SUM(a.sd2) FILTER (WHERE a.dir='Development') AS d_sd2, SUM(a.sd3) FILTER (WHERE a.dir='Development') AS d_sd3, SUM(a.sd4) FILTER (WHERE a.dir='Development') AS d_sd4, SUM(a.sg1) FILTER (WHERE a.dir='Development') AS d_sg1, SUM(a.sg2) FILTER (WHERE a.dir='Development') AS d_sg2, SUM(a.sg3) FILTER (WHERE a.dir='Development') AS d_sg3, SUM(a.sg4) FILTER (WHERE a.dir='Development') AS d_sg4, SUM(a.rq1) FILTER (WHERE a.dir='Development') AS d_rq1, SUM(a.rq2) FILTER (WHERE a.dir='Development') AS d_rq2, SUM(a.rq3) FILTER (WHERE a.dir='Development') AS d_rq3, SUM(a.rq4) FILTER (WHERE a.dir='Development') AS d_rq4, SUM(a.fq1) FILTER (WHERE a.dir='Development') AS d_fq1, SUM(a.fq2) FILTER (WHERE a.dir='Development') AS d_fq2, SUM(a.fq3) FILTER (WHERE a.dir='Development') AS d_fq3, SUM(a.fq4) FILTER (WHERE a.dir='Development') AS d_fq4, SUM(a.t1q2) FILTER (WHERE a.dir='Development') AS d_t1q2, SUM(a.t1q3) FILTER (WHERE a.dir='Development') AS d_t1q3, SUM(a.t1q4) FILTER (WHERE a.dir='Development') AS d_t1q4, SUM(a.t2q3) FILTER (WHERE a.dir='Development') AS d_t2q3, SUM(a.t2q4) FILTER (WHERE a.dir='Development') AS d_t2q4, SUM(a.t3q4) FILTER (WHERE a.dir='Development') AS d_t3q4, SUM(a.adj1) FILTER (WHERE a.dir='Development') AS d_adj1, SUM(a.adj2) FILTER (WHERE a.dir='Development') AS d_adj2, SUM(a.adj3) FILTER (WHERE a.dir='Development') AS d_adj3, SUM(a.adj4) FILTER (WHERE a.dir='Development') AS d_adj4, SUM(a.rev2) FILTER (WHERE a.dir='Development') AS d_rev2, SUM(a.rev3) FILTER (WHERE a.dir='Development') AS d_rev3, SUM(a.rev4) FILTER (WHERE a.dir='Development') AS d_rev4 FROM v3.mv_expense_item_tree t JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code LEFT JOIN line_agg a ON a.eid = ANY(t.desc_ids) WHERE t.depth <= 2 GROUP BY t.id, t.depth, t.item_id, t.name ) SELECT CASE r.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' END::VARCHAR, r.depth, r.section_code::VARCHAR, r.name::VARCHAR, -- Plan supp / dev r.s_pq1, r.s_pq2, r.s_pq3, r.s_pq4, COALESCE(r.s_pq1,0)+COALESCE(r.s_pq2,0)+COALESCE(r.s_pq3,0)+COALESCE(r.s_pq4,0), r.d_pq1, r.d_pq2, r.d_pq3, r.d_pq4, COALESCE(r.d_pq1,0)+COALESCE(r.d_pq2,0)+COALESCE(r.d_pq3,0)+COALESCE(r.d_pq4,0), -- Approved supp = plan + seq_dfip + seq_ssp + reserve COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0), COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0), COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0), COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0), COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0) +COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0) +COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0) +COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0), -- Approved dev COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0), COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0), COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0), COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0), COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0) +COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0) +COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0) +COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0), -- Fact supp / dev r.s_fq1, r.s_fq2, r.s_fq3, r.s_fq4, COALESCE(r.s_fq1,0)+COALESCE(r.s_fq2,0)+COALESCE(r.s_fq3,0)+COALESCE(r.s_fq4,0), r.d_fq1, r.d_fq2, r.d_fq3, r.d_fq4, COALESCE(r.d_fq1,0)+COALESCE(r.d_fq2,0)+COALESCE(r.d_fq3,0)+COALESCE(r.d_fq4,0), -- Corrected supp Q2..Q4 = approved_qN + transfers_into_qN + revisions + adj COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0) +COALESCE(r.s_t1q2,0)+COALESCE(r.s_rev2,0)+COALESCE(r.s_adj2,0), COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0) +COALESCE(r.s_t1q3,0)+COALESCE(r.s_t2q3,0)+COALESCE(r.s_rev3,0)+COALESCE(r.s_adj3,0), COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0) +COALESCE(r.s_t1q4,0)+COALESCE(r.s_t2q4,0)+COALESCE(r.s_t3q4,0)+COALESCE(r.s_rev4,0)+COALESCE(r.s_adj4,0), -- Corrected dev Q2..Q4 COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0) +COALESCE(r.d_t1q2,0)+COALESCE(r.d_rev2,0)+COALESCE(r.d_adj2,0), COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0) +COALESCE(r.d_t1q3,0)+COALESCE(r.d_t2q3,0)+COALESCE(r.d_rev3,0)+COALESCE(r.d_adj3,0), COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0) +COALESCE(r.d_t1q4,0)+COALESCE(r.d_t2q4,0)+COALESCE(r.d_t3q4,0)+COALESCE(r.d_rev4,0)+COALESCE(r.d_adj4,0) FROM tw_rollup r ORDER BY r.section_code; END; $function$ ; -- DROP FUNCTION v3.v_form2_view(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form2_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql STABLE AS $function$ BEGIN -- ═══ Основные листы АХР / КВ_П / Операц ════════════════════════════════ IF p_sheet IN ('AHR','CAP','OPER') THEN RETURN QUERY SELECT j.row_type, j.depth, ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order, jsonb_build_object( 'line_id', j.line_id, 'header', j.header, 'plan', j.plan_data, 'seq_dfip', j.seq_dfip_data, 'seq_ssp', j.seq_ssp_data, 'approved', j.approved_data, 'contract', j.contract_data, 'booking', j.booking_data, 'q1', j.q1_data, 'q2', j.q2_data, 'q3', j.q3_data, 'q4', j.q4_data, 'totals', j.totals_data ) AS data FROM v3.v_form2_sheet_jsonb(p_form_id, p_sheet, p_sections) j ORDER BY j._sort_path; RETURN; END IF; -- ═══ АХР_лимит ═════════════════════════════════════════════════════════ -- SECTION/GROUP-строки агрегируют qty/amount по всем LEAF-потомкам -- (Excel: J6=SUM(J7:J13) и т.д.). Дерево строится через parent_id. -- -- Структура (limit_template) — глобальный каталог нормативов, единый на банк. -- Значения qty/comment — per-form (form_limit ON budget_form_id = p_form_id). -- amount_qN — computed по формуле: -- amount_qN = qty_qN * limit_with_vat * period_factor(unit), где -- period_factor = 1/4 если unit ~ '/год' -- 3 если unit ~ '/месяц' (3 месяца в квартале) -- 1 иначе (по умолчанию /квартал) IF p_sheet = 'AHR_LIMIT' THEN RETURN QUERY WITH RECURSIVE tree AS ( SELECT id AS root_id, id AS desc_id FROM v3.limit_template UNION SELECT t.root_id, child.id FROM tree t JOIN v3.limit_template child ON child.parent_id = t.desc_id ), leaf_amt AS ( -- per-LEAF computed amount по формуле; qty берётся из form_limit для p_form_id SELECT lt.id, fl.qty_q1, fl.qty_q2, fl.qty_q3, fl.qty_q4, COALESCE(fl.qty_q1,0) * COALESCE(lt.limit_with_vat,0) * CASE WHEN lt.unit LIKE '%/год' THEN 0.25 WHEN lt.unit LIKE '%/месяц' THEN 3 ELSE 1 END AS amount_q1, COALESCE(fl.qty_q2,0) * COALESCE(lt.limit_with_vat,0) * CASE WHEN lt.unit LIKE '%/год' THEN 0.25 WHEN lt.unit LIKE '%/месяц' THEN 3 ELSE 1 END AS amount_q2, COALESCE(fl.qty_q3,0) * COALESCE(lt.limit_with_vat,0) * CASE WHEN lt.unit LIKE '%/год' THEN 0.25 WHEN lt.unit LIKE '%/месяц' THEN 3 ELSE 1 END AS amount_q3, COALESCE(fl.qty_q4,0) * COALESCE(lt.limit_with_vat,0) * CASE WHEN lt.unit LIKE '%/год' THEN 0.25 WHEN lt.unit LIKE '%/месяц' THEN 3 ELSE 1 END AS amount_q4 FROM v3.limit_template lt LEFT JOIN v3.form_limit fl ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id WHERE lt.row_type = 'LEAF' ), agg AS ( SELECT t.root_id, SUM(la.qty_q1) AS qty_q1, SUM(la.amount_q1) AS amount_q1, SUM(la.qty_q2) AS qty_q2, SUM(la.amount_q2) AS amount_q2, SUM(la.qty_q3) AS qty_q3, SUM(la.amount_q3) AS amount_q3, SUM(la.qty_q4) AS qty_q4, SUM(la.amount_q4) AS amount_q4 FROM tree t JOIN leaf_amt la ON la.id = t.desc_id GROUP BY t.root_id ) SELECT -- Маппинг физического хранения limit_template → DTO: -- SECTION → ROOT, GROUP → GROUP, LEAF → INPUT (редактируемая ячейка). CASE lt.row_type WHEN 'SECTION' THEN 'ROOT' WHEN 'GROUP' THEN 'GROUP' WHEN 'LEAF' THEN 'INPUT' END::VARCHAR, CASE lt.row_type WHEN 'SECTION' THEN 0 WHEN 'GROUP' THEN 1 ELSE 2 END, lt.sort_order::BIGINT, jsonb_build_object( 'id', lt.id, -- LEAF id для адресации в write API (= template_id) 'section_no', lt.section_no, 'expense_item_code',lt.expense_item_code, 'name', lt.name, 'unit', lt.unit, 'limit_with_vat', lt.limit_with_vat, 'limit_without_vat',lt.limit_without_vat, 'qty_q1', a.qty_q1, 'amount_q1', a.amount_q1, 'qty_q2', a.qty_q2, 'amount_q2', a.amount_q2, 'qty_q3', a.qty_q3, 'amount_q3', a.amount_q3, 'qty_q4', a.qty_q4, 'amount_q4', a.amount_q4, 'comment', fl.comment ) FROM v3.limit_template lt LEFT JOIN agg a ON a.root_id = lt.id LEFT JOIN v3.form_limit fl ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id ORDER BY lt.sort_order; RETURN; END IF; -- ═══ АХР_аренда ════════════════════════════════════════════════════════ IF p_sheet = 'AHR_RENT' THEN RETURN QUERY SELECT 'INPUT'::VARCHAR, 0, ROW_NUMBER() OVER (ORDER BY rd.id) AS sort_order, jsonb_build_object( 'id', rd.id, -- id rent_detail (для write API) 'line_id', rd.line_id, 'vsp_id', rd.vsp_id, 'address', v.address, 'object_type', v.format, 'rented_area', v.total_area, 'contract_number', rd.contract_number, 'contract_end_date',rd.contract_end_date, 'plan', jsonb_build_object('q1', rd.plan_q1, 'q2', rd.plan_q2, 'q3', rd.plan_q3, 'q4', rd.plan_q4, 'year', COALESCE(rd.plan_q1,0)+COALESCE(rd.plan_q2,0)+COALESCE(rd.plan_q3,0)+COALESCE(rd.plan_q4,0)), 'fact_q1', jsonb_build_object('jan', rd.actual_jan, 'feb', rd.actual_feb, 'mar', rd.actual_mar, 'total', COALESCE(rd.actual_jan,0)+COALESCE(rd.actual_feb,0)+COALESCE(rd.actual_mar,0)), 'fact_q2', jsonb_build_object('apr', rd.actual_apr, 'may', rd.actual_may, 'jun', rd.actual_jun, 'total', COALESCE(rd.actual_apr,0)+COALESCE(rd.actual_may,0)+COALESCE(rd.actual_jun,0)), 'fact_q3', jsonb_build_object('jul', rd.actual_jul, 'aug', rd.actual_aug, 'sep', rd.actual_sep, 'total', COALESCE(rd.actual_jul,0)+COALESCE(rd.actual_aug,0)+COALESCE(rd.actual_sep,0)), 'fact_q4', jsonb_build_object('oct', rd.actual_oct, 'nov', rd.actual_nov, 'dec', rd.actual_dec, 'total', COALESCE(rd.actual_oct,0)+COALESCE(rd.actual_nov,0)+COALESCE(rd.actual_dec,0)), 'comment', rd.comment ) FROM v3.rent_detail rd JOIN v3.budget_line bl ON bl.id = rd.line_id JOIN v3.vsp v ON v.id = rd.vsp_id WHERE bl.budget_form_id = p_form_id ORDER BY rd.id; RETURN; END IF; -- ═══ АХР_коммунал ═══════════════════════════════════════════════════════ IF p_sheet = 'AHR_UTILITY' THEN RETURN QUERY SELECT 'INPUT'::VARCHAR, 0, ROW_NUMBER() OVER (ORDER BY ud.id) AS sort_order, jsonb_build_object( 'id', ud.id, -- id utility_detail (для write API) 'line_id', ud.line_id, 'vsp_id', ud.vsp_id, 'address', v.address, 'object_type', v.format, 'object_area', v.total_area, 'contract_number', ud.contract_number, 'contract_end_date',ud.contract_end_date, 'plan', jsonb_build_object('q1', ud.plan_q1, 'q2', ud.plan_q2, 'q3', ud.plan_q3, 'q4', ud.plan_q4, 'year', COALESCE(ud.plan_q1,0)+COALESCE(ud.plan_q2,0)+COALESCE(ud.plan_q3,0)+COALESCE(ud.plan_q4,0)), 'fact_q1', jsonb_build_object('jan', ud.actual_jan, 'feb', ud.actual_feb, 'mar', ud.actual_mar, 'total', COALESCE(ud.actual_jan,0)+COALESCE(ud.actual_feb,0)+COALESCE(ud.actual_mar,0)), 'fact_q2', jsonb_build_object('apr', ud.actual_apr, 'may', ud.actual_may, 'jun', ud.actual_jun, 'total', COALESCE(ud.actual_apr,0)+COALESCE(ud.actual_may,0)+COALESCE(ud.actual_jun,0)), 'fact_q3', jsonb_build_object('jul', ud.actual_jul, 'aug', ud.actual_aug, 'sep', ud.actual_sep, 'total', COALESCE(ud.actual_jul,0)+COALESCE(ud.actual_aug,0)+COALESCE(ud.actual_sep,0)), 'fact_q4', jsonb_build_object('oct', ud.actual_oct, 'nov', ud.actual_nov, 'dec', ud.actual_dec, 'total', COALESCE(ud.actual_oct,0)+COALESCE(ud.actual_nov,0)+COALESCE(ud.actual_dec,0)), 'comment', ud.comment ) FROM v3.utility_detail ud JOIN v3.budget_line bl ON bl.id = ud.line_id JOIN v3.vsp v ON v.id = ud.vsp_id WHERE bl.budget_form_id = p_form_id ORDER BY ud.id; RETURN; END IF; -- ═══ АХР_охрана ═════════════════════════════════════════════════════════ IF p_sheet = 'AHR_SECURITY' THEN RETURN QUERY SELECT 'INPUT'::VARCHAR, 0, ROW_NUMBER() OVER (ORDER BY sd.id) AS sort_order, jsonb_build_object( 'id', sd.id, -- id security_detail (для write API) 'line_id', sd.line_id, 'vsp_id', sd.vsp_id, 'address', v.address, 'object_type', v.format, 'object_area', v.total_area, 'contract_number', sd.contract_number, 'contract_end_date',sd.contract_end_date, 'plan', jsonb_build_object('q1', sd.plan_q1, 'q2', sd.plan_q2, 'q3', sd.plan_q3, 'q4', sd.plan_q4, 'year', COALESCE(sd.plan_q1,0)+COALESCE(sd.plan_q2,0)+COALESCE(sd.plan_q3,0)+COALESCE(sd.plan_q4,0)), 'fact_q1', jsonb_build_object('jan', sd.actual_jan, 'feb', sd.actual_feb, 'mar', sd.actual_mar, 'total', COALESCE(sd.actual_jan,0)+COALESCE(sd.actual_feb,0)+COALESCE(sd.actual_mar,0)), 'fact_q2', jsonb_build_object('apr', sd.actual_apr, 'may', sd.actual_may, 'jun', sd.actual_jun, 'total', COALESCE(sd.actual_apr,0)+COALESCE(sd.actual_may,0)+COALESCE(sd.actual_jun,0)), 'fact_q3', jsonb_build_object('jul', sd.actual_jul, 'aug', sd.actual_aug, 'sep', sd.actual_sep, 'total', COALESCE(sd.actual_jul,0)+COALESCE(sd.actual_aug,0)+COALESCE(sd.actual_sep,0)), 'fact_q4', jsonb_build_object('oct', sd.actual_oct, 'nov', sd.actual_nov, 'dec', sd.actual_dec, 'total', COALESCE(sd.actual_oct,0)+COALESCE(sd.actual_nov,0)+COALESCE(sd.actual_dec,0)), 'comment', sd.comment ) FROM v3.security_detail sd JOIN v3.budget_line bl ON bl.id = sd.line_id JOIN v3.vsp v ON v.id = sd.vsp_id WHERE bl.budget_form_id = p_form_id ORDER BY sd.id; RETURN; END IF; -- ═══ Отч.9ф (Fixed Asset Report) ════════════════════════════════════════ -- Одна строка = (expense_item × equipment_name × month). -- -- opening_qty/amt_604/60415 хранятся в БД ТОЛЬКО для month=1 (база года). -- Для month=2..12 вычисляются на лету: накопительная сумма дельт по -- предыдущим месяцам от base (Excel: opening_M2 = F1+H1−J1). -- -- totals.* — также computed (acquired_total, disposed_total, balance_qty/amt). IF p_sheet = 'OTCH9F' THEN RETURN QUERY WITH base AS ( SELECT far.id, far.budget_form_id, far.expense_item_id, far.equipment_name, far.month, far.acquired_qty_604, far.acquired_amt_604, far.disposed_qty_604, far.disposed_amt_604, far.acquired_qty_60415, far.acquired_amt_60415, far.transferred_qty_60415, far.transferred_amt_60415, far.go_balance_only_amt, -- база: opening_qty/amt из month=1 этой группы FIRST_VALUE(COALESCE(far.opening_qty_604,0)) OVER w AS base_qty_604, FIRST_VALUE(COALESCE(far.opening_amt_604,0)) OVER w AS base_amt_604, FIRST_VALUE(COALESCE(far.opening_qty_60415,0)) OVER w AS base_qty_60415, FIRST_VALUE(COALESCE(far.opening_amt_60415,0)) OVER w AS base_amt_60415, -- кумулятивная дельта по предыдущим месяцам в группе COALESCE(SUM(COALESCE(far.acquired_qty_604,0) - COALESCE(far.disposed_qty_604,0)) OVER w_prev, 0) AS cum_dq_604, COALESCE(SUM(COALESCE(far.acquired_amt_604,0) - COALESCE(far.disposed_amt_604,0)) OVER w_prev, 0) AS cum_da_604, COALESCE(SUM(COALESCE(far.acquired_qty_60415,0) - COALESCE(far.transferred_qty_60415,0)) OVER w_prev, 0) AS cum_dq_60415, COALESCE(SUM(COALESCE(far.acquired_amt_60415,0) - COALESCE(far.transferred_amt_60415,0)) OVER w_prev, 0) AS cum_da_60415 FROM v3.fixed_asset_report far WHERE far.budget_form_id = p_form_id WINDOW w AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name ORDER BY far.month), w_prev AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name ORDER BY far.month ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) ), eff AS ( SELECT b.*, (b.base_qty_604 + b.cum_dq_604) AS opening_qty_604_eff, (b.base_amt_604 + b.cum_da_604) AS opening_amt_604_eff, (b.base_qty_60415 + b.cum_dq_60415) AS opening_qty_60415_eff, (b.base_amt_60415 + b.cum_da_60415) AS opening_amt_60415_eff FROM base b ) SELECT 'INPUT'::VARCHAR, 0, ROW_NUMBER() OVER (ORDER BY eff.expense_item_id, eff.month) AS sort_order, jsonb_build_object( 'id', eff.id, -- fixed_asset_report.id (для write API) 'expense_item_id', ei.id, 'item_id', ei.item_id, 'section_code', ei.section_code, 'equipment_name', eff.equipment_name, 'month', eff.month, 'b_604', jsonb_build_object( 'opening_qty', eff.opening_qty_604_eff, 'opening_amt', eff.opening_amt_604_eff, 'acquired_qty', eff.acquired_qty_604, 'acquired_amt', eff.acquired_amt_604, 'disposed_qty', eff.disposed_qty_604, 'disposed_amt', eff.disposed_amt_604 ), 'b_60415', jsonb_build_object( 'opening_qty', eff.opening_qty_60415_eff, 'opening_amt', eff.opening_amt_60415_eff, 'acquired_qty', eff.acquired_qty_60415, 'acquired_amt', eff.acquired_amt_60415, 'transferred_qty',eff.transferred_qty_60415, 'transferred_amt',eff.transferred_amt_60415 ), 'totals', jsonb_build_object( -- Excel R = SUM(I, O, S) − Q 'acquired_total', COALESCE(eff.acquired_amt_604,0)+COALESCE(eff.acquired_amt_60415,0) +COALESCE(eff.go_balance_only_amt,0)-COALESCE(eff.transferred_amt_60415,0), 'go_balance_only',eff.go_balance_only_amt, -- Excel T = K 'disposed_total', COALESCE(eff.disposed_amt_604,0), -- Excel U = (opening+acquired−disposed)_604 + (opening+acquired−transferred)_60415 'balance_qty', (eff.opening_qty_604_eff + COALESCE(eff.acquired_qty_604,0) - COALESCE(eff.disposed_qty_604,0)) + (eff.opening_qty_60415_eff + COALESCE(eff.acquired_qty_60415,0) - COALESCE(eff.transferred_qty_60415,0)), 'balance_amt', (eff.opening_amt_604_eff + COALESCE(eff.acquired_amt_604,0) - COALESCE(eff.disposed_amt_604,0)) + (eff.opening_amt_60415_eff + COALESCE(eff.acquired_amt_60415,0) - COALESCE(eff.transferred_amt_60415,0)) ) ) FROM eff JOIN v3.expense_item ei ON ei.id = eff.expense_item_id ORDER BY eff.expense_item_id, eff.month; RETURN; END IF; -- ═══ Смета_2026 ═════════════════════════════════════════════════════════ IF p_sheet = 'SMETA' THEN RETURN QUERY SELECT sm.row_type, sm.depth, ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, jsonb_build_object( 'section_code', sm.section_code, 'name', sm.name, 'plan', jsonb_build_object( 'support', jsonb_build_object('q1', sm.plan_supp_q1, 'q2', sm.plan_supp_q2, 'q3', sm.plan_supp_q3, 'q4', sm.plan_supp_q4, 'year', sm.plan_supp_year), 'development', jsonb_build_object('q1', sm.plan_dev_q1, 'q2', sm.plan_dev_q2, 'q3', sm.plan_dev_q3, 'q4', sm.plan_dev_q4, 'year', sm.plan_dev_year) ), 'approved', jsonb_build_object( 'support', jsonb_build_object('q1', sm.appr_supp_q1, 'q2', sm.appr_supp_q2, 'q3', sm.appr_supp_q3, 'q4', sm.appr_supp_q4, 'year', sm.appr_supp_year), 'development', jsonb_build_object('q1', sm.appr_dev_q1, 'q2', sm.appr_dev_q2, 'q3', sm.appr_dev_q3, 'q4', sm.appr_dev_q4, 'year', sm.appr_dev_year) ), 'fact', jsonb_build_object( 'support', jsonb_build_object('q1', sm.fact_supp_q1, 'q2', sm.fact_supp_q2, 'q3', sm.fact_supp_q3, 'q4', sm.fact_supp_q4, 'year', sm.fact_supp_year), 'development', jsonb_build_object('q1', sm.fact_dev_q1, 'q2', sm.fact_dev_q2, 'q3', sm.fact_dev_q3, 'q4', sm.fact_dev_q4, 'year', sm.fact_dev_year) ), -- corrected: только Q2/Q3/Q4 (см. шаблон Смета_2026, блок V-X). Q1 нет -- (Q1 corrected ≡ approved_q1), year нет (q1+q2+q3+q4 даёт двойной счёт переносов). 'corrected', jsonb_build_object( 'support', jsonb_build_object('q2', sm.corr_supp_q2, 'q3', sm.corr_supp_q3, 'q4', sm.corr_supp_q4), 'development', jsonb_build_object('q2', sm.corr_dev_q2, 'q3', sm.corr_dev_q3, 'q4', sm.corr_dev_q4) ) ) FROM v3.v_form2_smeta(p_form_id) sm ORDER BY sm.section_code; RETURN; END IF; RAISE EXCEPTION 'Unknown sheet: %', p_sheet USING HINT = 'Use AHR / CAP / OPER / AHR_LIMIT / AHR_RENT / AHR_UTILITY / AHR_SECURITY / OTCH9F / SMETA'; END; $function$ ; -- DROP FUNCTION v3.v_form3_report_sections(int4, _text); CREATE OR REPLACE FUNCTION v3.v_form3_report_sections(p_report_id integer, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_q1_adj_by_items numeric, col_q1_adj_increase numeric, col_q1_total_corr numeric, col_q1_m1 numeric, col_q1_m2 numeric, col_q1_m3 numeric, col_q1_quarter_actual numeric, col_q1_economy numeric, col_q2_carryover numeric, col_q2_adj_by_items numeric, col_q2_adj_increase numeric, col_q2_total_corr numeric, col_q2_m1 numeric, col_q2_m2 numeric, col_q2_m3 numeric, col_q2_quarter_actual numeric, col_q2_economy numeric, col_q3_carryover numeric, col_q3_adj_by_items numeric, col_q3_adj_increase numeric, col_q3_total_corr numeric, col_q3_m1 numeric, col_q3_m2 numeric, col_q3_m3 numeric, col_q3_quarter_actual numeric, col_q3_economy numeric, col_q4_carryover numeric, col_q4_adj_by_items numeric, col_q4_adj_increase numeric, col_q4_total_corr numeric, col_q4_m1 numeric, col_q4_m2 numeric, col_q4_m3 numeric, col_q4_spod numeric, col_q4_quarter_actual numeric, col_q4_economy numeric, col_year_total_corr numeric, col_year_total_actual numeric, col_year_economy numeric, _sort_path integer[]) LANGUAGE plpgsql STABLE AS $function$ DECLARE s_q1 BOOLEAN := p_sections IS NULL OR 'q1' = ANY(p_sections); s_q2 BOOLEAN := p_sections IS NULL OR 'q2' = ANY(p_sections); s_q3 BOOLEAN := p_sections IS NULL OR 'q3' = ANY(p_sections); s_q4 BOOLEAN := p_sections IS NULL OR 'q4' = ANY(p_sections); s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections); BEGIN IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; RETURN QUERY WITH -- Дерево expense_item, ограниченное FORM_3 (через junction) tw AS ( SELECT t.* FROM v3.mv_expense_item_tree t JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3' ), -- INPUT строки: одна на v3.rf_project_report_line page AS ( SELECT l.id AS lid, l.expense_item_id AS eid, t.parent_item_id AS sc, -- section-код родителя в дереве (1.01.1.) ei.item_id AS ic, ei.num_group_id AS ng, ei.name AS ename, t.path AS tree_path FROM v3.rf_project_report_line l 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 ), -- Поквартальные данные jq AS ( SELECT q.rf_project_report_line_id AS lid, q.quarter, q.adj_by_items, q.adj_increase, q.actual_m1, q.actual_m2, q.actual_m3, q.actual_spod FROM v3.rf_project_report_quarter q JOIN v3.rf_project_report_line l ON l.id = q.rf_project_report_line_id WHERE l.rf_project_report_id = p_report_id ), -- INPUT base: соединяем page + 4 квартала input_base AS ( SELECT pg.lid, pg.eid, pg.sc, pg.ic, pg.ng, pg.ename, pg.tree_path, q1.adj_by_items AS q1_abi, q1.adj_increase AS q1_ai, q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, q2.adj_by_items AS q2_abi, q2.adj_increase AS q2_ai, q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, q3.adj_by_items AS q3_abi, q3.adj_increase AS q3_ai, q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, q4.adj_by_items AS q4_abi, q4.adj_increase AS q4_ai, q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod FROM page pg LEFT JOIN jq q1 ON q1.lid = pg.lid AND q1.quarter = 1 LEFT JOIN jq q2 ON q2.lid = pg.lid AND q2.quarter = 2 LEFT JOIN jq q3 ON q3.lid = pg.lid AND q3.quarter = 3 LEFT JOIN jq q4 ON q4.lid = pg.lid AND q4.quarter = 4 ), -- Computed totals per row enriched AS ( SELECT b.*, COALESCE(b.q1_abi,0) + COALESCE(b.q1_ai,0) AS q1_tc, COALESCE(b.q1_m1,0) + COALESCE(b.q1_m2,0) + COALESCE(b.q1_m3,0) AS q1_qa, COALESCE(b.q2_abi,0) + COALESCE(b.q2_ai,0) AS q2_tc, COALESCE(b.q2_m1,0) + COALESCE(b.q2_m2,0) + COALESCE(b.q2_m3,0) AS q2_qa, COALESCE(b.q3_abi,0) + COALESCE(b.q3_ai,0) AS q3_tc, COALESCE(b.q3_m1,0) + COALESCE(b.q3_m2,0) + COALESCE(b.q3_m3,0) AS q3_qa, COALESCE(b.q4_abi,0) + COALESCE(b.q4_ai,0) AS q4_tc, COALESCE(b.q4_m1,0) + COALESCE(b.q4_m2,0) + COALESCE(b.q4_m3,0) + COALESCE(b.q4_spod,0) AS q4_qa FROM input_base b ), -- Per-line aggregates for tree rollup agg_q AS ( SELECT l.expense_item_id AS eid, q.quarter, SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai, SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, SUM(q.actual_spod) AS spod FROM v3.rf_project_report_line l JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id WHERE l.rf_project_report_id = p_report_id GROUP BY l.expense_item_id, q.quarter ), tw_aq1 AS ( SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 1 GROUP BY tw.id ), tw_aq2 AS ( SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 2 GROUP BY tw.id ), tw_aq3 AS ( SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 3 GROUP BY tw.id ), tw_aq4 AS ( SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3, SUM(a.spod) AS spod FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 4 GROUP BY tw.id ) -- ═══ Часть A: INPUT строки ════════════════════════════════════════════════ SELECT * FROM ( SELECT 'INPUT'::VARCHAR AS row_type, 3 AS depth, e.lid::INT AS line_id, e.sc, e.ic, e.ng, e.ename, -- Q1 CASE WHEN s_q1 THEN e.q1_abi END, CASE WHEN s_q1 THEN e.q1_ai END, CASE WHEN s_q1 THEN e.q1_tc END, CASE WHEN s_q1 THEN e.q1_m1 END, CASE WHEN s_q1 THEN e.q1_m2 END, CASE WHEN s_q1 THEN e.q1_m3 END, CASE WHEN s_q1 THEN e.q1_qa END, CASE WHEN s_q1 THEN ROUND(e.q1_tc - e.q1_qa, 1) END, -- Q2 (carryover пока NULL — нет столбца в _quarter; задел на будущее) CASE WHEN s_q2 THEN NULL::NUMERIC END, CASE WHEN s_q2 THEN e.q2_abi END, CASE WHEN s_q2 THEN e.q2_ai END, CASE WHEN s_q2 THEN e.q2_tc END, CASE WHEN s_q2 THEN e.q2_m1 END, CASE WHEN s_q2 THEN e.q2_m2 END, CASE WHEN s_q2 THEN e.q2_m3 END, CASE WHEN s_q2 THEN e.q2_qa END, CASE WHEN s_q2 THEN ROUND(e.q2_tc - e.q2_qa, 1) END, -- Q3 CASE WHEN s_q3 THEN NULL::NUMERIC END, CASE WHEN s_q3 THEN e.q3_abi END, CASE WHEN s_q3 THEN e.q3_ai END, CASE WHEN s_q3 THEN e.q3_tc END, CASE WHEN s_q3 THEN e.q3_m1 END, CASE WHEN s_q3 THEN e.q3_m2 END, CASE WHEN s_q3 THEN e.q3_m3 END, CASE WHEN s_q3 THEN e.q3_qa END, CASE WHEN s_q3 THEN ROUND(e.q3_tc - e.q3_qa, 1) END, -- Q4 CASE WHEN s_q4 THEN NULL::NUMERIC END, CASE WHEN s_q4 THEN e.q4_abi END, CASE WHEN s_q4 THEN e.q4_ai END, CASE WHEN s_q4 THEN e.q4_tc END, CASE WHEN s_q4 THEN e.q4_m1 END, CASE WHEN s_q4 THEN e.q4_m2 END, CASE WHEN s_q4 THEN e.q4_m3 END, CASE WHEN s_q4 THEN e.q4_spod END, CASE WHEN s_q4 THEN e.q4_qa END, CASE WHEN s_q4 THEN ROUND(e.q4_tc - e.q4_qa, 1) END, -- year CASE WHEN s_year THEN e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc END, CASE WHEN s_year THEN e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa END, CASE WHEN s_year THEN ROUND((e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc) - (e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa), 1) END, e.tree_path || ARRAY[e.lid::INT] AS _sort_path FROM enriched e UNION ALL -- ═══ Часть B: Иерархия (ROOT / GROUP / ITEM / SUB_ITEM) ═════════════════ SELECT CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR, t.depth, NULL::INT, CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END, CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END, t.num_group_id, t.name, -- Q1 CASE WHEN s_q1 THEN a1.abi END, CASE WHEN s_q1 THEN a1.ai END, CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END, CASE WHEN s_q1 THEN a1.m1 END, CASE WHEN s_q1 THEN a1.m2 END, CASE WHEN s_q1 THEN a1.m3 END, CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END, CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0)) - (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END, -- Q2 CASE WHEN s_q2 THEN NULL::NUMERIC END, CASE WHEN s_q2 THEN a2.abi END, CASE WHEN s_q2 THEN a2.ai END, CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END, CASE WHEN s_q2 THEN a2.m1 END, CASE WHEN s_q2 THEN a2.m2 END, CASE WHEN s_q2 THEN a2.m3 END, CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END, CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0)) - (COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END, -- Q3 CASE WHEN s_q3 THEN NULL::NUMERIC END, CASE WHEN s_q3 THEN a3.abi END, CASE WHEN s_q3 THEN a3.ai END, CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END, CASE WHEN s_q3 THEN a3.m1 END, CASE WHEN s_q3 THEN a3.m2 END, CASE WHEN s_q3 THEN a3.m3 END, CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END, CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0)) - (COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END, -- Q4 CASE WHEN s_q4 THEN NULL::NUMERIC END, CASE WHEN s_q4 THEN a4.abi END, CASE WHEN s_q4 THEN a4.ai END, CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END, CASE WHEN s_q4 THEN a4.m1 END, CASE WHEN s_q4 THEN a4.m2 END, CASE WHEN s_q4 THEN a4.m3 END, CASE WHEN s_q4 THEN a4.spod END, CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END, CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) - (COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END, -- year CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+ COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+ COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+ COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END, CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+ COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+ COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+ COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END, CASE WHEN s_year THEN ROUND( (COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+ COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) - (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+ COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+ COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+ COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END, t.path FROM tw t LEFT JOIN tw_aq1 a1 ON a1.id = t.id LEFT JOIN tw_aq2 a2 ON a2.id = t.id LEFT JOIN tw_aq3 a3 ON a3.id = t.id LEFT JOIN tw_aq4 a4 ON a4.id = t.id ) sub ORDER BY sub._sort_path; END; $function$ ; -- DROP FUNCTION v3.v_form3_report_jsonb(int4, _text); CREATE OR REPLACE FUNCTION v3.v_form3_report_jsonb(p_report_id integer, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql STABLE AS $function$ SELECT s.row_type, s.depth, ROW_NUMBER() OVER (ORDER BY s._sort_path) AS sort_order, jsonb_build_object( 'line_id', s.line_id, 'header', jsonb_build_object( 'section_code', s.col_section_code, 'item_id', s.col_item_id, 'num_group_id', s.col_num_group_id, 'name', s.col_name, -- expense_item_id из _sort_path: для INPUT путь = {...ei_id, line_id}, -- для иерархии = {...ei_id} → последний/предпоследний элемент. 'expense_item_id', CASE WHEN s.row_type = 'INPUT' THEN s._sort_path[array_upper(s._sort_path,1)-1] ELSE s._sort_path[array_upper(s._sort_path,1)] END ), 'q1', jsonb_build_object( 'adj_by_items', s.col_q1_adj_by_items, 'adj_increase', s.col_q1_adj_increase, 'total_corr', s.col_q1_total_corr, 'm1', s.col_q1_m1, 'm2', s.col_q1_m2, 'm3', s.col_q1_m3, 'quarter_actual', s.col_q1_quarter_actual, 'economy', s.col_q1_economy ), 'q2', jsonb_build_object( 'carryover', s.col_q2_carryover, 'adj_by_items', s.col_q2_adj_by_items, 'adj_increase', s.col_q2_adj_increase, 'total_corr', s.col_q2_total_corr, 'm1', s.col_q2_m1, 'm2', s.col_q2_m2, 'm3', s.col_q2_m3, 'quarter_actual', s.col_q2_quarter_actual, 'economy', s.col_q2_economy ), 'q3', jsonb_build_object( 'carryover', s.col_q3_carryover, 'adj_by_items', s.col_q3_adj_by_items, 'adj_increase', s.col_q3_adj_increase, 'total_corr', s.col_q3_total_corr, 'm1', s.col_q3_m1, 'm2', s.col_q3_m2, 'm3', s.col_q3_m3, 'quarter_actual', s.col_q3_quarter_actual, 'economy', s.col_q3_economy ), 'q4', jsonb_build_object( 'carryover', s.col_q4_carryover, 'adj_by_items', s.col_q4_adj_by_items, 'adj_increase', s.col_q4_adj_increase, 'total_corr', s.col_q4_total_corr, 'm1', s.col_q4_m1, 'm2', s.col_q4_m2, 'm3', s.col_q4_m3, 'spod', s.col_q4_spod, 'quarter_actual', s.col_q4_quarter_actual, 'economy', s.col_q4_economy ), 'totals', jsonb_build_object( 'total_corr', s.col_year_total_corr, 'total_actual', s.col_year_total_actual, 'economy', s.col_year_economy ) ) AS data FROM v3.v_form3_report_sections(p_report_id, p_sections) s ORDER BY s._sort_path; $function$ ; -- DROP FUNCTION v3.v_form3_rf_rollup_sections(int4, int4, _text); CREATE OR REPLACE FUNCTION v3.v_form3_rf_rollup_sections(p_branch_id integer, p_year integer, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_q1_adj_by_items numeric, col_q1_adj_increase numeric, col_q1_total_corr numeric, col_q1_m1 numeric, col_q1_m2 numeric, col_q1_m3 numeric, col_q1_quarter_actual numeric, col_q1_economy numeric, col_q2_adj_by_items numeric, col_q2_adj_increase numeric, col_q2_total_corr numeric, col_q2_m1 numeric, col_q2_m2 numeric, col_q2_m3 numeric, col_q2_quarter_actual numeric, col_q2_economy numeric, col_q3_adj_by_items numeric, col_q3_adj_increase numeric, col_q3_total_corr numeric, col_q3_m1 numeric, col_q3_m2 numeric, col_q3_m3 numeric, col_q3_quarter_actual numeric, col_q3_economy numeric, col_q4_adj_by_items numeric, col_q4_adj_increase numeric, col_q4_total_corr numeric, col_q4_m1 numeric, col_q4_m2 numeric, col_q4_m3 numeric, col_q4_spod numeric, col_q4_quarter_actual numeric, col_q4_economy numeric, col_year_total_corr numeric, col_year_total_actual numeric, col_year_economy numeric, _sort_path integer[]) LANGUAGE plpgsql STABLE AS $function$ DECLARE s_q1 BOOLEAN := p_sections IS NULL OR 'q1' = ANY(p_sections); s_q2 BOOLEAN := p_sections IS NULL OR 'q2' = ANY(p_sections); s_q3 BOOLEAN := p_sections IS NULL OR 'q3' = ANY(p_sections); s_q4 BOOLEAN := p_sections IS NULL OR 'q4' = ANY(p_sections); s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections); BEGIN RETURN QUERY WITH tw AS ( SELECT t.* FROM v3.mv_expense_item_tree t JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3' ), -- Агрегаты per (expense_item_id, quarter) — sum по обоим трекам всех проектов РФ × год agg_q AS ( SELECT l.expense_item_id AS eid, q.quarter, SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai, SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, SUM(q.actual_spod) AS spod FROM v3.rf_project_report r JOIN v3.project p ON p.id = r.project_id JOIN v3.rf_project_report_line l ON l.rf_project_report_id = r.id JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id WHERE p.org_unit_id = p_branch_id AND r.year = p_year GROUP BY l.expense_item_id, q.quarter ), tw_aq1 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=1 GROUP BY tw.id), tw_aq2 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=2 GROUP BY tw.id), tw_aq3 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=3 GROUP BY tw.id), tw_aq4 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3, SUM(a.spod) AS spod FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=4 GROUP BY tw.id) SELECT CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR, t.depth, CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END, CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END, t.num_group_id, t.name, -- Q1 CASE WHEN s_q1 THEN a1.abi END, CASE WHEN s_q1 THEN a1.ai END, CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END, CASE WHEN s_q1 THEN a1.m1 END, CASE WHEN s_q1 THEN a1.m2 END, CASE WHEN s_q1 THEN a1.m3 END, CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END, CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0)) -(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END, -- Q2 CASE WHEN s_q2 THEN a2.abi END, CASE WHEN s_q2 THEN a2.ai END, CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END, CASE WHEN s_q2 THEN a2.m1 END, CASE WHEN s_q2 THEN a2.m2 END, CASE WHEN s_q2 THEN a2.m3 END, CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END, CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0)) -(COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END, -- Q3 CASE WHEN s_q3 THEN a3.abi END, CASE WHEN s_q3 THEN a3.ai END, CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END, CASE WHEN s_q3 THEN a3.m1 END, CASE WHEN s_q3 THEN a3.m2 END, CASE WHEN s_q3 THEN a3.m3 END, CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END, CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0)) -(COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END, -- Q4 CASE WHEN s_q4 THEN a4.abi END, CASE WHEN s_q4 THEN a4.ai END, CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END, CASE WHEN s_q4 THEN a4.m1 END, CASE WHEN s_q4 THEN a4.m2 END, CASE WHEN s_q4 THEN a4.m3 END, CASE WHEN s_q4 THEN a4.spod END, CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END, CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) -(COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END, -- year CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+ COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END, CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+ COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+ COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+ COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END, CASE WHEN s_year THEN ROUND( (COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+ COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) -(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+ COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+ COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+ COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END, t.path FROM tw t LEFT JOIN tw_aq1 a1 ON a1.id = t.id LEFT JOIN tw_aq2 a2 ON a2.id = t.id LEFT JOIN tw_aq3 a3 ON a3.id = t.id LEFT JOIN tw_aq4 a4 ON a4.id = t.id ORDER BY t.path; END; $function$ ; -- DROP FUNCTION v3.v_form3_rf_rollup_jsonb(int4, int4, _text); CREATE OR REPLACE FUNCTION v3.v_form3_rf_rollup_jsonb(p_branch_id integer, p_year integer, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql STABLE AS $function$ SELECT s.row_type, s.depth, ROW_NUMBER() OVER (ORDER BY s._sort_path) AS sort_order, jsonb_build_object( 'header', jsonb_build_object( 'section_code', s.col_section_code, 'item_id', s.col_item_id, 'num_group_id', s.col_num_group_id, 'name', s.col_name ), 'q1', jsonb_build_object( 'adj_by_items', s.col_q1_adj_by_items, 'adj_increase', s.col_q1_adj_increase, 'total_corr', s.col_q1_total_corr, 'm1', s.col_q1_m1, 'm2', s.col_q1_m2, 'm3', s.col_q1_m3, 'quarter_actual', s.col_q1_quarter_actual, 'economy', s.col_q1_economy ), 'q2', jsonb_build_object( 'adj_by_items', s.col_q2_adj_by_items, 'adj_increase', s.col_q2_adj_increase, 'total_corr', s.col_q2_total_corr, 'm1', s.col_q2_m1, 'm2', s.col_q2_m2, 'm3', s.col_q2_m3, 'quarter_actual', s.col_q2_quarter_actual, 'economy', s.col_q2_economy ), 'q3', jsonb_build_object( 'adj_by_items', s.col_q3_adj_by_items, 'adj_increase', s.col_q3_adj_increase, 'total_corr', s.col_q3_total_corr, 'm1', s.col_q3_m1, 'm2', s.col_q3_m2, 'm3', s.col_q3_m3, 'quarter_actual', s.col_q3_quarter_actual, 'economy', s.col_q3_economy ), 'q4', jsonb_build_object( 'adj_by_items', s.col_q4_adj_by_items, 'adj_increase', s.col_q4_adj_increase, 'total_corr', s.col_q4_total_corr, 'm1', s.col_q4_m1, 'm2', s.col_q4_m2, 'm3', s.col_q4_m3, 'spod', s.col_q4_spod, 'quarter_actual', s.col_q4_quarter_actual, 'economy', s.col_q4_economy ), 'totals', jsonb_build_object( 'total_corr', s.col_year_total_corr, 'total_actual', s.col_year_total_actual, 'economy', s.col_year_economy ) ) AS data FROM v3.v_form3_rf_rollup_sections(p_branch_id, p_year, p_sections) s ORDER BY s._sort_path; $function$ ; -- DROP FUNCTION v3.v_form3_smeta(int4, int4); CREATE OR REPLACE FUNCTION v3.v_form3_smeta(p_branch_id integer, p_year integer) RETURNS TABLE(row_type character varying, section_code character varying, name character varying, total_corr numeric, total_actual numeric, economy numeric) LANGUAGE sql STABLE AS $function$ -- Top-level sections (ROOT) + общая итоговая SELECT row_type, col_section_code, col_name, col_year_total_corr, col_year_total_actual, col_year_economy FROM v3.v_form3_rf_rollup_sections(p_branch_id, p_year, ARRAY['year']) WHERE depth = 0 UNION ALL -- Итог: «Всего расходов по смете (п.1+п.3)» SELECT 'TOTAL'::VARCHAR, NULL::VARCHAR, 'Всего расходов по смете (п.1+п.3)'::VARCHAR, SUM(col_year_total_corr), SUM(col_year_total_actual), ROUND(SUM(col_year_total_corr) - SUM(col_year_total_actual), 1) FROM v3.v_form3_rf_rollup_sections(p_branch_id, p_year, ARRAY['year']) WHERE depth = 0; $function$ ; -- DROP FUNCTION v3.v_form4_sheet_sections(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form4_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_justification character varying, col_internal_order character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cs_amount numeric, col_cs_reference character varying, col_cs_counterparty character varying, col_cs_deadline character varying, col_cs_comment character varying, col_cs_future_y1 numeric, col_cs_future_y2 numeric, col_cs_other_ssp numeric, col_cs_centralized_flag character varying, col_al_contract_ref character varying, col_al_purpose character varying, col_rsv_q1 numeric, col_rsv_q2 numeric, col_rsv_q3 numeric, col_rsv_q4 numeric, col_rsv_year numeric, col_rsv_just character varying, col_col_amount numeric, col_col_protocol character varying, col_col_note character varying, col_ckk_ceiling numeric, col_ckk_q1 numeric, col_ckk_q2 numeric, col_ckk_q3 numeric, col_ckk_q4 numeric, col_ckk_rf_schedule character varying, col_ckk_delivery_deadline character varying, col_ckk_procurement_plan character varying, col_ckk_procurement_method character varying, col_ckk_comment character varying, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_addenda character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_q1 numeric, col_cd_q2 numeric, col_cd_q3 numeric, col_cd_q4 numeric, col_cd_rf_schedule character varying, col_cd_vat_rate character varying, col_cd_exchange_rate numeric, col_cd_amount_foreign numeric, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_rf numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_ho numeric, col_q1_pay_rf numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_rf numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_ho numeric, col_q2_pay_rf numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_rf numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_ho numeric, col_q3_pay_rf numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_rf numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_ho numeric, col_q4_pay_rf numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, _sort_path integer[]) LANGUAGE plpgsql STABLE AS $function$ #variable_conflict use_column DECLARE s_plan BOOL; s_seq_d BOOL; s_appr BOOL; s_cd BOOL; s_book BOOL; s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; s_cs BOOL; s_al BOOL; s_rsv BOOL; s_col BOOL; s_ckk BOOL; s_need_ap BOOL; -- plan/seq/reserve нужны для approved + cp1..cp4 BEGIN s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); s_seq_d := p_sections IS NULL OR 'seq_dfip' = ANY(p_sections); s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); s_cd := p_sections IS NULL OR 'contract' = ANY(p_sections); s_book := p_sections IS NULL OR 'booking' = ANY(p_sections); s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); s_cs := p_sections IS NULL OR 'contract_summary' = ANY(p_sections); s_al := p_sections IS NULL OR 'allocation' = ANY(p_sections); s_rsv := p_sections IS NULL OR 'reserve' = ANY(p_sections); s_col := p_sections IS NULL OR 'collegial' = ANY(p_sections); s_ckk := p_sections IS NULL OR 'ckk' = ANY(p_sections); s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; RETURN QUERY WITH tw AS ( SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids FROM v3.mv_expense_item_tree t JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code WHERE t.sheet = p_sheet ), page AS ( SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.name AS bname, bl.vsp_id FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet ), -- Conditional joins (gated by section flags) jp AS (SELECT p.* FROM v3.plan p JOIN page pg ON p.line_id = pg.lid WHERE s_need_ap), jsd AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='DFIP'), jr AS (SELECT r.* FROM v3.reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_need_ap), jcd AS (SELECT c.* FROM v3.contract_detail c JOIN page pg ON c.line_id = pg.lid WHERE s_cd), jck AS (SELECT c.* FROM v3.ckk c JOIN page pg ON c.line_id = pg.lid WHERE s_book OR s_ckk), jcs AS (SELECT c.* FROM v3.contract_summary c JOIN page pg ON c.line_id = pg.lid WHERE s_cs), jal AS (SELECT a.* FROM v3.allocation a JOIN page pg ON a.line_id = pg.lid WHERE s_al), jcol AS (SELECT c.* FROM v3.collegial_approval c JOIN page pg ON c.line_id = pg.lid WHERE s_col), jq1 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q1 AND q.quarter=1), jq2 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q2 AND q.quarter=2), jq3 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q3 AND q.quarter=3), jq4 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE (s_q4 OR s_tot) AND q.quarter=4), -- INPUT строки: enriched данные input_base AS ( SELECT pg.lid, pg.eid, pg.bname, pg.vsp_id, v.address AS vsp_addr, bl_just.justification AS just, bl_just.internal_order AS io, t.parent_item_id AS sc, t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, t.path AS tree_path, -- plan COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, p.comment AS pcmt, -- seq dfip / ssp_go COALESCE(sd.adj_q1,0) AS dq1, COALESCE(sd.adj_q2,0) AS dq2, COALESCE(sd.adj_q3,0) AS dq3, COALESCE(sd.adj_q4,0) AS dq4, sd.justification AS djust, -- reserve COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, -- contract (расширено: q1-4, addenda, exchange_rate, amount_foreign) cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.addenda AS cd_add, cd.contract_date AS cd_dt, cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil, cd.expenses_q1 AS cd_q1, cd.expenses_q2 AS cd_q2, cd.expenses_q3 AS cd_q3, cd.expenses_q4 AS cd_q4, cd.rf_schedule AS cd_rfsch, cd.vat_rate AS cd_vat, cd.exchange_rate AS cd_xr, cd.amount_foreign AS cd_af, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, cd.act AS cd_act, cd.comment AS cd_cmt, -- ckk (booking из expenses_q* + полный блок: ceiling, rf_schedule, …) ck.expenses_q1 AS bk1, ck.expenses_q2 AS bk2, ck.expenses_q3 AS bk3, ck.expenses_q4 AS bk4, ck.expenses_next_year_q1 AS bn1, ck.expenses_next_year_q2 AS bn2, ck.expenses_next_year_q3 AS bn3, ck.expenses_next_year_q4 AS bn4, ck.ceiling_amount AS ck_ceil, ck.rf_schedule AS ck_rfsch, ck.delivery_deadline AS ck_dd, ck.procurement_plan AS ck_pp, ck.procurement_method AS ck_pm, ck.comment AS ck_cmt, -- contract_summary (Действующий договор) cs.total_amount AS cs_amt, cs.reference AS cs_ref, cs.counterparty AS cs_cp, cs.deadline AS cs_dl, cs.comment AS cs_cmt, cs.future_payments_y1 AS cs_y1, cs.future_payments_y2 AS cs_y2, cs.other_ssp_amount AS cs_oss, cs.centralized_flag AS cs_cflag, -- allocation al.contract_ref AS al_ref, al.allocation_purpose AS al_purp, -- collegial col.approved_amount AS col_amt, col.protocol_reference AS col_pr, col.note AS col_note, -- Q1 q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_rf AS q1_arf, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt, q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, q1.payment_comment AS q1_pcmt, q1.payment_act AS q1_pact, q1.booking_amount AS q1_book, q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, q1.transfer_to_economy AS q1_te, q1.transfer_delay_acts AS q1_tda, q1.transfer_delay_procurement AS q1_tdp, q1.transfer_economy_rf AS q1_terf, q1.transfer_next_comment AS q1_tnc, q1.transfer_far_comment AS q1_tfc, -- Q2 q2.target_change AS q2_tc, q2.base_plan_correction AS q2_bc, q2.base_plan_correction_comment AS q2_bcc, q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, q2.plan_revision_comment AS q2_rcmt, q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_rf AS q2_arf, q2.adj_reserve AS q2_arv, q2.adj_comment AS q2_acmt, q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf, q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, q2.booking_amount AS q2_book, q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, q2.transfer_to_economy AS q2_te, q2.transfer_delay_acts AS q2_tda, q2.transfer_delay_procurement AS q2_tdp, q2.transfer_economy_rf AS q2_terf, q2.transfer_next_comment AS q2_tnc, q2.transfer_far_comment AS q2_tfc, -- Q3 q3.target_change AS q3_tc, q3.base_plan_correction AS q3_bc, q3.base_plan_correction_comment AS q3_bcc, q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, q3.plan_revision_comment AS q3_rcmt, q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_rf AS q3_arf, q3.adj_reserve AS q3_arv, q3.adj_comment AS q3_acmt, q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf, q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, q3.booking_amount AS q3_book, q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, q3.transfer_to_q4 AS q3_tq4, q3.transfer_to_economy AS q3_te, q3.transfer_delay_acts AS q3_tda, q3.transfer_delay_procurement AS q3_tdp, q3.transfer_economy_rf AS q3_terf, q3.transfer_next_comment AS q3_tnc, -- Q4 q4.target_change AS q4_tc, q4.base_plan_correction AS q4_bc, q4.base_plan_correction_comment AS q4_bcc, q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, q4.plan_revision_comment AS q4_rcmt, q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_rf AS q4_arf, q4.adj_reserve AS q4_arv, q4.adj_comment AS q4_acmt, q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf, q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, q4.booking_amount AS q4_book, q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod, q4.transfer_to_economy AS q4_te, -- approved per quarter COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4 FROM page pg JOIN tw t ON t.id = pg.eid LEFT JOIN v3.vsp v ON v.id = pg.vsp_id LEFT JOIN v3.budget_line bl_just ON bl_just.id = pg.lid LEFT JOIN jp p ON p.line_id = pg.lid LEFT JOIN jsd sd ON sd.line_id = pg.lid LEFT JOIN jr r ON r.line_id = pg.lid LEFT JOIN jcd cd ON cd.line_id = pg.lid LEFT JOIN jck ck ON ck.line_id = pg.lid LEFT JOIN jcs cs ON cs.line_id = pg.lid LEFT JOIN jal al ON al.line_id = pg.lid LEFT JOIN jcol col ON col.line_id = pg.lid LEFT JOIN jq1 q1 ON q1.line_id = pg.lid LEFT JOIN jq2 q2 ON q2.line_id = pg.lid LEFT JOIN jq3 q3 ON q3.line_id = pg.lid LEFT JOIN jq4 q4 ON q4.line_id = pg.lid ), -- Computed: corrected/new plans -- Excel: CH10 (cp1) = AM10 + SUM(CB:CE) − CF10 -- DJ10 (np2) = SUM(AN10, CY10, DH10, CF10, DE10:DG10) -- В Excel CY и CF — две отдельные колонки (CY = «Закрытие 1-го квартала / Перенос -- во 2 кв», CF = плановый «Перенос во 2 кв» внутри корректировок 1 кв). В DB -- хранится одно поле q1.transfer_to_q2 — оно работает «за двоих»: вычитается -- из cp1 (как CF) и прибавляется к np2 (как CY). Допущение: CY = CF. enriched AS ( SELECT b.*, -- Q1 corrected_plan = approved + adj_* − transfer_to_q2 b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_arf,0)+COALESCE(b.q1_arv,0) - COALESCE(b.q1_tq2,0) AS cp1, COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, -- Q2 new_plan = approved + transfer_q1 + revision_inc/seq + base_correction + target_change b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) + COALESCE(b.q2_bc,0)+COALESCE(b.q2_tc,0) AS np2, COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) + COALESCE(b.q3_bc,0)+COALESCE(b.q3_tc,0) AS np3, COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) + COALESCE(b.q4_bc,0)+COALESCE(b.q4_tc,0) AS np4, COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 FROM input_base b ), final_input AS ( SELECT e.*, -- Q2 cp = new_plan + adj_* − transfer_to_q3 e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_arf,0)+COALESCE(e.q2_arv,0) - COALESCE(e.q2_tq3,0) AS cp2, -- Q3 cp = new_plan + adj_* − transfer_to_q4 e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_arf,0)+COALESCE(e.q3_arv,0) - COALESCE(e.q3_tq4,0) AS cp3, -- Q4 cp = new_plan + adj_* (нет дальнейшего переноса) e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_arf,0)+COALESCE(e.q4_arv,0) AS cp4 FROM enriched e ), -- Aggregates per expense_item (для иерархии) agg AS ( SELECT bl.expense_item_id AS eid, SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, SUM(sd.adj_q1) AS sd1, SUM(sd.adj_q2) AS sd2, SUM(sd.adj_q3) AS sd3, SUM(sd.adj_q4) AS sd4, SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, SUM(ck.expenses_q1) AS bk1, SUM(ck.expenses_q2) AS bk2, SUM(ck.expenses_q3) AS bk3, SUM(ck.expenses_q4) AS bk4, SUM(ck.expenses_next_year_q1) AS bn1, SUM(ck.expenses_next_year_q2) AS bn2, SUM(ck.expenses_next_year_q3) AS bn3, SUM(ck.expenses_next_year_q4) AS bn4 FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN v3.plan p ON p.line_id = bl.id AND s_need_ap LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor='DFIP' AND s_need_ap LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_need_ap OR s_book) GROUP BY bl.expense_item_id ), -- Per-quarter aggregates from blq aq_q AS ( SELECT bl.expense_item_id AS eid, q.quarter, SUM(q.adj_current) AS ac, SUM(q.adj_ssp) AS as_v, SUM(q.adj_rf) AS arf, SUM(q.adj_reserve) AS arv, SUM(q.payment_amount) AS pa, SUM(q.payment_amount_ho) AS pho, SUM(q.payment_amount_rf) AS prf, SUM(q.booking_amount) AS book, SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, SUM(q.actual_spod) AS spod, SUM(q.transfer_to_q2) AS tq2, SUM(q.transfer_to_q3) AS tq3, SUM(q.transfer_to_q4) AS tq4, SUM(q.transfer_to_economy) AS te, -- Δ к approved для перехода к new_plan: revision_inc/seq + target_change + base_correction -- (Excel: DJ10 = SUM(AN, CY, DH, CF, DE:DG)). Должно совпадать с INPUT-формулой np. SUM(COALESCE(q.plan_revision_increase,0) + COALESCE(q.plan_revision_sequester,0) + COALESCE(q.target_change,0) + COALESCE(q.base_plan_correction,0)) AS rev FROM v3.budget_line bl JOIN v3.expense_item ei ON ei.id = bl.expense_item_id JOIN v3.budget_line_quarter q ON q.line_id = bl.id WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet GROUP BY bl.expense_item_id, q.quarter ), -- Tree-rollup tw_agg AS ( SELECT tw.id, SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, SUM(a.sd1) AS sd1, SUM(a.sd2) AS sd2, SUM(a.sd3) AS sd3, SUM(a.sd4) AS sd4, SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, SUM(a.bk1) AS bk1, SUM(a.bk2) AS bk2, SUM(a.bk3) AS bk3, SUM(a.bk4) AS bk4, SUM(a.bn1) AS bn1, SUM(a.bn2) AS bn2, SUM(a.bn3) AS bn3, SUM(a.bn4) AS bn4 FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) GROUP BY tw.id ), tw_aq1 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.book) AS book, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=1 GROUP BY tw.id ), tw_aq2 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.book) AS book, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=2 GROUP BY tw.id ), tw_aq3 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.book) AS book, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=3 GROUP BY tw.id ), tw_aq4 AS ( SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, SUM(b.pa) AS pa, SUM(b.book) AS book, SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod, SUM(b.te) AS te, SUM(b.rev) AS rev FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=4 GROUP BY tw.id ) -- ═══ Часть A: INPUT строки ═════════════════════════════════════════════ SELECT * FROM ( SELECT 'INPUT'::VARCHAR, 3, f.lid::INT, f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.just, f.io, f.vsp_id, f.vsp_addr, -- plan CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, -- seq dfip CASE WHEN s_seq_d THEN f.dq1 END, CASE WHEN s_seq_d THEN f.dq2 END, CASE WHEN s_seq_d THEN f.dq3 END, CASE WHEN s_seq_d THEN f.dq4 END, CASE WHEN s_seq_d THEN f.dq1+f.dq2+f.dq3+f.dq4 END, CASE WHEN s_seq_d THEN f.djust END, -- approved CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, -- contract_summary (Действующий договор) CASE WHEN s_cs THEN f.cs_amt END, CASE WHEN s_cs THEN f.cs_ref END, CASE WHEN s_cs THEN f.cs_cp END, CASE WHEN s_cs THEN f.cs_dl END, CASE WHEN s_cs THEN f.cs_cmt END, CASE WHEN s_cs THEN f.cs_y1 END, CASE WHEN s_cs THEN f.cs_y2 END, CASE WHEN s_cs THEN f.cs_oss END, CASE WHEN s_cs THEN f.cs_cflag END, -- allocation CASE WHEN s_al THEN f.al_ref END, CASE WHEN s_al THEN f.al_purp END, -- reserve (отдельный блок) CASE WHEN s_rsv THEN f.rq1 END, CASE WHEN s_rsv THEN f.rq2 END, CASE WHEN s_rsv THEN f.rq3 END, CASE WHEN s_rsv THEN f.rq4 END, CASE WHEN s_rsv THEN f.rq1+f.rq2+f.rq3+f.rq4 END, NULL::VARCHAR, -- collegial CASE WHEN s_col THEN f.col_amt END, CASE WHEN s_col THEN f.col_pr END, CASE WHEN s_col THEN f.col_note END, -- ckk полный CASE WHEN s_ckk THEN f.ck_ceil END, CASE WHEN s_ckk THEN f.bk1 END, CASE WHEN s_ckk THEN f.bk2 END, CASE WHEN s_ckk THEN f.bk3 END, CASE WHEN s_ckk THEN f.bk4 END, CASE WHEN s_ckk THEN f.ck_rfsch END, CASE WHEN s_ckk THEN f.ck_dd END, CASE WHEN s_ckk THEN f.ck_pp END, CASE WHEN s_ckk THEN f.ck_pm END, CASE WHEN s_ckk THEN f.ck_cmt END, -- contract (расширено) CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, CASE WHEN s_cd THEN f.cd_add END, CASE WHEN s_cd THEN f.cd_dt END, CASE WHEN s_cd THEN f.cd_subj END, CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, CASE WHEN s_cd THEN f.cd_q1 END, CASE WHEN s_cd THEN f.cd_q2 END, CASE WHEN s_cd THEN f.cd_q3 END, CASE WHEN s_cd THEN f.cd_q4 END, CASE WHEN s_cd THEN f.cd_rfsch END, CASE WHEN s_cd THEN f.cd_vat END, CASE WHEN s_cd THEN f.cd_xr END, CASE WHEN s_cd THEN f.cd_af END, CASE WHEN s_cd THEN f.cd_dl END, CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END, CASE WHEN s_cd THEN f.cd_cmt END, -- booking 2026 CASE WHEN s_book THEN f.bk1 END, CASE WHEN s_book THEN f.bk2 END, CASE WHEN s_book THEN f.bk3 END, CASE WHEN s_book THEN f.bk4 END, -- booking 2027 CASE WHEN s_book THEN f.bn1 END, CASE WHEN s_book THEN f.bn2 END, CASE WHEN s_book THEN f.bn3 END, CASE WHEN s_book THEN f.bn4 END, -- Q1 CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, CASE WHEN s_q1 THEN f.q1_arf END, CASE WHEN s_q1 THEN f.q1_arv END, CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, CASE WHEN s_q1 THEN f.q1_pcmt END, CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN f.q1_book END, CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, CASE WHEN s_q1 THEN f.aq1 END, CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.q1_book,0) END, -- residual_after_booking CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, -- residual_after_actual CASE WHEN s_q1 THEN f.q1_tq2 END, CASE WHEN s_q1 THEN f.q1_tda END, CASE WHEN s_q1 THEN f.q1_tdp END, CASE WHEN s_q1 THEN f.q1_terf END, CASE WHEN s_q1 THEN f.q1_tnc END, CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, CASE WHEN s_q1 THEN f.q1_tfc END, CASE WHEN s_q1 THEN f.q1_te END, -- total = "Закрытие квартала" = сумма transfer-колонок CASE WHEN s_q1 THEN COALESCE(f.q1_tq2,0)+COALESCE(f.q1_tq3,0)+COALESCE(f.q1_tq4,0)+COALESCE(f.q1_te,0) END, -- Q2 CASE WHEN s_q2 THEN f.q2_tc END, CASE WHEN s_q2 THEN f.q2_bc END, CASE WHEN s_q2 THEN f.q2_bcc END, CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, CASE WHEN s_q2 THEN f.q2_rcmt END, CASE WHEN s_q2 THEN f.np2 END, CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, CASE WHEN s_q2 THEN f.q2_arf END, CASE WHEN s_q2 THEN f.q2_arv END, CASE WHEN s_q2 THEN f.q2_acmt END, CASE WHEN s_q2 THEN f.cp2 END, CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END, CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, CASE WHEN s_q2 THEN f.q2_book END, CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, CASE WHEN s_q2 THEN f.aq2 END, CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.q2_book,0) END, CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, CASE WHEN s_q2 THEN f.q2_tq3 END, CASE WHEN s_q2 THEN f.q2_tda END, CASE WHEN s_q2 THEN f.q2_tdp END, CASE WHEN s_q2 THEN f.q2_terf END, CASE WHEN s_q2 THEN f.q2_tnc END, CASE WHEN s_q2 THEN f.q2_tq4 END, CASE WHEN s_q2 THEN f.q2_tfc END, CASE WHEN s_q2 THEN f.q2_te END, CASE WHEN s_q2 THEN COALESCE(f.q2_tq3,0)+COALESCE(f.q2_tq4,0)+COALESCE(f.q2_te,0) END, -- Q3 CASE WHEN s_q3 THEN f.q3_tc END, CASE WHEN s_q3 THEN f.q3_bc END, CASE WHEN s_q3 THEN f.q3_bcc END, CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, CASE WHEN s_q3 THEN f.q3_rcmt END, CASE WHEN s_q3 THEN f.np3 END, CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, CASE WHEN s_q3 THEN f.q3_arf END, CASE WHEN s_q3 THEN f.q3_arv END, CASE WHEN s_q3 THEN f.q3_acmt END, CASE WHEN s_q3 THEN f.cp3 END, CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END, CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, CASE WHEN s_q3 THEN f.q3_book END, CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, CASE WHEN s_q3 THEN f.aq3 END, CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.q3_book,0) END, CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, CASE WHEN s_q3 THEN f.q3_tq4 END, CASE WHEN s_q3 THEN f.q3_tda END, CASE WHEN s_q3 THEN f.q3_tdp END, CASE WHEN s_q3 THEN f.q3_terf END, CASE WHEN s_q3 THEN f.q3_tnc END, CASE WHEN s_q3 THEN f.q3_te END, CASE WHEN s_q3 THEN COALESCE(f.q3_tq4,0)+COALESCE(f.q3_te,0) END, -- Q4 CASE WHEN s_q4 THEN f.q4_tc END, CASE WHEN s_q4 THEN f.q4_bc END, CASE WHEN s_q4 THEN f.q4_bcc END, CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, CASE WHEN s_q4 THEN f.q4_rcmt END, CASE WHEN s_q4 THEN f.np4 END, CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, CASE WHEN s_q4 THEN f.q4_arf END, CASE WHEN s_q4 THEN f.q4_arv END, CASE WHEN s_q4 THEN f.q4_acmt END, CASE WHEN s_q4 THEN f.cp4 END, CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END, CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, CASE WHEN s_q4 THEN f.q4_book END, CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, CASE WHEN s_q4 THEN f.q4_spod END, CASE WHEN s_q4 THEN f.aq4 END, CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.q4_book,0) END, CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, CASE WHEN s_q4 THEN f.q4_te END, CASE WHEN s_q4 THEN COALESCE(f.q4_te,0) END, -- totals CASE WHEN s_tot THEN f.aq1+f.aq2+f.aq3+f.aq4 END, -- fact_year f.tree_path || ARRAY[f.lid::INT] AS _sort FROM final_input f UNION ALL -- ═══ Часть B: Иерархия ════════════════════════════════════════════════ SELECT CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR, t.depth, NULL::INT, CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END, CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END, t.num_group_id, t.name, NULL::VARCHAR, NULL::VARCHAR, -- justification, internal_order NULL::INT, NULL::VARCHAR, -- plan CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END, CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END, CASE WHEN s_plan THEN COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) END, NULL::VARCHAR, -- seq dfip CASE WHEN s_seq_d THEN ta.sd1 END, CASE WHEN s_seq_d THEN ta.sd2 END, CASE WHEN s_seq_d THEN ta.sd3 END, CASE WHEN s_seq_d THEN ta.sd4 END, CASE WHEN s_seq_d THEN COALESCE(ta.sd1,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sd4,0) END, NULL::VARCHAR, -- approved CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) END, CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) +COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) +COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) +COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) END, -- contract_summary (NULL на иерархии — текстовые) NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- allocation NULL::VARCHAR, NULL::VARCHAR, -- reserve (q1-4 + year агрегаты, justification NULL) CASE WHEN s_rsv THEN ta.sr1 END, CASE WHEN s_rsv THEN ta.sr2 END, CASE WHEN s_rsv THEN ta.sr3 END, CASE WHEN s_rsv THEN ta.sr4 END, CASE WHEN s_rsv THEN COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0) END, NULL::VARCHAR, -- collegial NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, -- ckk полный (q1-4 агрегаты есть в ta.bk*, остальное NULL) NULL::NUMERIC, CASE WHEN s_ckk THEN ta.bk1 END, CASE WHEN s_ckk THEN ta.bk2 END, CASE WHEN s_ckk THEN ta.bk3 END, CASE WHEN s_ckk THEN ta.bk4 END, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- contract (расширено) — все NULL на иерархии NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::DATE, NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- booking 2026 / 2027 CASE WHEN s_book THEN ta.bk1 END, CASE WHEN s_book THEN ta.bk2 END, CASE WHEN s_book THEN ta.bk3 END, CASE WHEN s_book THEN ta.bk4 END, CASE WHEN s_book THEN ta.bn1 END, CASE WHEN s_book THEN ta.bn2 END, CASE WHEN s_book THEN ta.bn3 END, CASE WHEN s_book THEN ta.bn4 END, -- Q1 hierarchy CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as_v END, CASE WHEN s_q1 THEN tb1.arf END, CASE WHEN s_q1 THEN tb1.arv END, NULL::VARCHAR, -- corrected_plan = approved + adj_* − transfer_to_q2 CASE WHEN s_q1 THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0) -COALESCE(tb1.tq2,0) END, NULL::DATE, CASE WHEN s_q1 THEN tb1.pa END, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, -- pay_act CASE WHEN s_q1 THEN tb1.book END, -- booking агрегат CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END, CASE WHEN s_q1 THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) END, -- residual_after_booking = cp − booking CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0) -COALESCE(tb1.tq2,0)) - COALESCE(tb1.book,0) END, -- residual_after_actual = cp − actual CASE WHEN s_q1 THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0) -COALESCE(tb1.tq2,0)) - (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) END, CASE WHEN s_q1 THEN tb1.tq2 END, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- tq2 reasons + next comment CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END, NULL::VARCHAR, -- far comment CASE WHEN s_q1 THEN tb1.te END, -- total = "Закрытие квартала" = сумма transfer-колонок CASE WHEN s_q1 THEN COALESCE(tb1.tq2,0)+COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)+COALESCE(tb1.te,0) END, -- Q2 hierarchy NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq NULL::VARCHAR, -- revision_comment CASE WHEN s_q2 THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) END, CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as_v END, CASE WHEN s_q2 THEN tb2.arf END, CASE WHEN s_q2 THEN tb2.arv END, NULL::VARCHAR, -- adj_comment -- corrected_plan = new_plan + adj_* − transfer_to_q3 CASE WHEN s_q2 THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0) -COALESCE(tb2.tq3,0) END, NULL::DATE, CASE WHEN s_q2 THEN tb2.pa END, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- pay_ho/pay_rf/pay_comment NULL::VARCHAR, -- pay_act CASE WHEN s_q2 THEN tb2.book END, CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END, CASE WHEN s_q2 THEN COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) END, -- residual_after_booking = cp − booking CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0) -COALESCE(tb2.tq3,0)) - COALESCE(tb2.book,0) END, -- residual_after_actual = cp − actual CASE WHEN s_q2 THEN (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0) -COALESCE(tb2.tq3,0)) - (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) END, CASE WHEN s_q2 THEN tb2.tq3 END, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- tq3 reasons + next comment CASE WHEN s_q2 THEN tb2.tq4 END, NULL::VARCHAR, -- far comment CASE WHEN s_q2 THEN tb2.te END, -- total = сумма transfer-колонок q2 CASE WHEN s_q2 THEN COALESCE(tb2.tq3,0)+COALESCE(tb2.tq4,0)+COALESCE(tb2.te,0) END, -- Q3 hierarchy NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq NULL::VARCHAR, CASE WHEN s_q3 THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) END, CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as_v END, CASE WHEN s_q3 THEN tb3.arf END, CASE WHEN s_q3 THEN tb3.arv END, NULL::VARCHAR, -- corrected_plan = new_plan + adj_* − transfer_to_q4 CASE WHEN s_q3 THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0) -COALESCE(tb3.tq4,0) END, NULL::DATE, CASE WHEN s_q3 THEN tb3.pa END, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, CASE WHEN s_q3 THEN tb3.book END, CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END, CASE WHEN s_q3 THEN COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) END, -- residual_after_booking = cp − booking CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0) -COALESCE(tb3.tq4,0)) - COALESCE(tb3.book,0) END, -- residual_after_actual = cp − actual CASE WHEN s_q3 THEN (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0) -COALESCE(tb3.tq4,0)) - (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) END, CASE WHEN s_q3 THEN tb3.tq4 END, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, CASE WHEN s_q3 THEN tb3.te END, -- total = сумма transfer-колонок q3 CASE WHEN s_q3 THEN COALESCE(tb3.tq4,0)+COALESCE(tb3.te,0) END, -- Q4 hierarchy NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq NULL::VARCHAR, CASE WHEN s_q4 THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) END, CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as_v END, CASE WHEN s_q4 THEN tb4.arf END, CASE WHEN s_q4 THEN tb4.arv END, NULL::VARCHAR, -- corrected_plan = new_plan + adj_* (нет дальнейшего переноса) CASE WHEN s_q4 THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0) END, NULL::DATE, CASE WHEN s_q4 THEN tb4.pa END, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, CASE WHEN s_q4 THEN tb4.book END, CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END, CASE WHEN s_q4 THEN tb4.spod END, CASE WHEN s_q4 THEN COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, -- residual_after_booking = cp − booking CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0)) - COALESCE(tb4.book,0) END, -- residual_after_actual = cp − actual CASE WHEN s_q4 THEN (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0)) - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) END, CASE WHEN s_q4 THEN tb4.te END, -- total = "Закрытие" q4 = transfer_to_economy CASE WHEN s_q4 THEN COALESCE(tb4.te,0) END, -- totals CASE WHEN s_tot THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) +COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) +COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) +COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, -- fact_year t.path AS _sort FROM tw t LEFT JOIN tw_agg ta ON ta.id = t.id LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id ) sub ORDER BY sub._sort; END; $function$ ; -- DROP FUNCTION v3.v_form4_sheet_lines_jsonb(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form4_sheet_lines_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, approved_data jsonb, contract_summary_data jsonb, allocation_data jsonb, reserve_data jsonb, collegial_data jsonb, ckk_data jsonb, contract_data jsonb, booking_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[]) LANGUAGE sql STABLE AS $function$ WITH flags AS ( SELECT (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, (p_sections IS NULL OR 'seq_dfip' = ANY(p_sections)) AS s_seq_d, (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs, (p_sections IS NULL OR 'allocation' = ANY(p_sections)) AS s_al, (p_sections IS NULL OR 'reserve' = ANY(p_sections)) AS s_rsv, (p_sections IS NULL OR 'collegial' = ANY(p_sections)) AS s_col, (p_sections IS NULL OR 'ckk' = ANY(p_sections)) AS s_ckk, (p_sections IS NULL OR 'contract' = ANY(p_sections)) AS s_cd, (p_sections IS NULL OR 'booking' = ANY(p_sections)) AS s_book, (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot ) SELECT s.row_type, s.depth, s.line_id, -- header (всегда). expense_item_id из _sort_path (см. v_form1_jsonb). jsonb_build_object( 'section_code', s.col_section_code, 'item_id', s.col_item_id, 'num_group', s.col_num_group_id, 'name', s.col_name, 'justification', s.col_justification, 'internal_order', s.col_internal_order, 'vsp_id', s.col_vsp_id, 'vsp_address', s.col_vsp_address, 'expense_item_id', CASE WHEN s.row_type = 'INPUT' THEN s._sort_path[array_upper(s._sort_path,1)-1] ELSE s._sort_path[array_upper(s._sort_path,1)] END ) AS header, CASE WHEN f.s_plan THEN jsonb_build_object( 'q1', s.col_plan_q1, 'q2', s.col_plan_q2, 'q3', s.col_plan_q3, 'q4', s.col_plan_q4, 'year', s.col_plan_year, 'comment', s.col_plan_comment ) END AS plan_data, CASE WHEN f.s_seq_d THEN jsonb_build_object( 'q1', s.col_seq_dfip_q1, 'q2', s.col_seq_dfip_q2, 'q3', s.col_seq_dfip_q3, 'q4', s.col_seq_dfip_q4, 'year', s.col_seq_dfip_year, 'justification', s.col_seq_dfip_just ) END AS seq_dfip_data, CASE WHEN f.s_appr THEN jsonb_build_object( 'q1', s.col_appr_q1, 'q2', s.col_appr_q2, 'q3', s.col_appr_q3, 'q4', s.col_appr_q4, 'year', s.col_appr_year ) END AS approved_data, CASE WHEN f.s_cs THEN jsonb_build_object( 'amount', s.col_cs_amount, 'reference', s.col_cs_reference, 'counterparty', s.col_cs_counterparty, 'deadline', s.col_cs_deadline, 'comment', s.col_cs_comment, 'future_payments_y1', s.col_cs_future_y1, 'future_payments_y2', s.col_cs_future_y2, 'other_ssp_amount', s.col_cs_other_ssp, 'centralized_flag', s.col_cs_centralized_flag ) END AS contract_summary_data, CASE WHEN f.s_al THEN jsonb_build_object( 'contract_ref', s.col_al_contract_ref, 'allocation_purpose', s.col_al_purpose ) END AS allocation_data, CASE WHEN f.s_rsv THEN jsonb_build_object( 'q1', s.col_rsv_q1, 'q2', s.col_rsv_q2, 'q3', s.col_rsv_q3, 'q4', s.col_rsv_q4, 'year', s.col_rsv_year, 'justification', s.col_rsv_just ) END AS reserve_data, CASE WHEN f.s_col THEN jsonb_build_object( 'amount', s.col_col_amount, 'protocol_reference', s.col_col_protocol, 'note', s.col_col_note ) END AS collegial_data, CASE WHEN f.s_ckk THEN jsonb_build_object( 'ceiling', s.col_ckk_ceiling, 'expenses_q1', s.col_ckk_q1, 'expenses_q2', s.col_ckk_q2, 'expenses_q3', s.col_ckk_q3, 'expenses_q4', s.col_ckk_q4, 'rf_schedule', s.col_ckk_rf_schedule, 'delivery_deadline', s.col_ckk_delivery_deadline, 'procurement_plan', s.col_ckk_procurement_plan, 'procurement_method', s.col_ckk_procurement_method, 'comment', s.col_ckk_comment ) END AS ckk_data, CASE WHEN f.s_cd THEN jsonb_build_object( 'counterparty', s.col_cd_counterparty, 'reference', s.col_cd_reference, 'addenda', s.col_cd_addenda, 'date', s.col_cd_date, 'subject', s.col_cd_subject, 'currency', s.col_cd_currency, 'ceiling', s.col_cd_ceiling, 'expenses_q1', s.col_cd_q1, 'expenses_q2', s.col_cd_q2, 'expenses_q3', s.col_cd_q3, 'expenses_q4', s.col_cd_q4, 'rf_schedule', s.col_cd_rf_schedule, 'vat_rate', s.col_cd_vat_rate, 'exchange_rate', s.col_cd_exchange_rate, 'amount_foreign', s.col_cd_amount_foreign, 'deadline', s.col_cd_deadline, 'scheme', s.col_cd_scheme, 'act', s.col_cd_act, 'comment', s.col_cd_comment ) END AS contract_data, CASE WHEN f.s_book THEN jsonb_build_object( 'y2026', jsonb_build_object('q1', s.col_book_q1, 'q2', s.col_book_q2, 'q3', s.col_book_q3, 'q4', s.col_book_q4), 'y2027', jsonb_build_object('q1', s.col_book_next_q1, 'q2', s.col_book_next_q2, 'q3', s.col_book_next_q3, 'q4', s.col_book_next_q4) ) END AS booking_data, CASE WHEN f.s_q1 THEN jsonb_build_object( 'adj_current', s.col_q1_adj_current, 'adj_ssp', s.col_q1_adj_ssp, 'adj_rf', s.col_q1_adj_rf, 'adj_reserve', s.col_q1_adj_reserve, 'adj_comment', s.col_q1_adj_comment, 'corrected_plan', s.col_q1_corrected_plan, 'pay_date', s.col_q1_pay_date, 'pay_amount', s.col_q1_pay_amount, 'pay_ho', s.col_q1_pay_ho, 'pay_rf', s.col_q1_pay_rf, 'pay_comment', s.col_q1_pay_comment, 'pay_act', s.col_q1_pay_act, 'booking', s.col_q1_booking, 'actual_m1', s.col_q1_actual_m1, 'actual_m2', s.col_q1_actual_m2, 'actual_m3', s.col_q1_actual_m3, 'actual_quarter', s.col_q1_actual_quarter, 'residual_after_booking', s.col_q1_residual_after_booking, 'residual_after_actual', s.col_q1_residual_after_actual, 'transfer_q2', s.col_q1_transfer_q2, 'transfer_q3', s.col_q1_transfer_q3, 'transfer_q4', s.col_q1_transfer_q4, 'transfer_econ', s.col_q1_transfer_econ, 'total', s.col_q1_total ) END AS q1_data, CASE WHEN f.s_q2 THEN jsonb_build_object( 'target_change', s.col_q2_target_change, 'base_correction', s.col_q2_base_correction, 'base_correction_comment', s.col_q2_base_correction_comment, 'revision_inc', s.col_q2_revision_inc, 'revision_seq', s.col_q2_revision_seq, 'revision_comment', s.col_q2_revision_comment, 'new_plan', s.col_q2_new_plan, 'adj_current', s.col_q2_adj_current, 'adj_ssp', s.col_q2_adj_ssp, 'adj_rf', s.col_q2_adj_rf, 'adj_reserve', s.col_q2_adj_reserve, 'adj_comment', s.col_q2_adj_comment, 'corrected_plan', s.col_q2_corrected_plan, 'pay_date', s.col_q2_pay_date, 'pay_amount', s.col_q2_pay_amount, 'pay_ho', s.col_q2_pay_ho, 'pay_rf', s.col_q2_pay_rf, 'pay_comment', s.col_q2_pay_comment, 'pay_act', s.col_q2_pay_act, 'booking', s.col_q2_booking, 'actual_m1', s.col_q2_actual_m1, 'actual_m2', s.col_q2_actual_m2, 'actual_m3', s.col_q2_actual_m3, 'actual_quarter', s.col_q2_actual_quarter, 'residual_after_booking', s.col_q2_residual_after_booking, 'residual_after_actual', s.col_q2_residual_after_actual, 'transfer_q3', s.col_q2_transfer_q3, 'transfer_q4', s.col_q2_transfer_q4, 'transfer_econ', s.col_q2_transfer_econ, 'total', s.col_q2_total ) END AS q2_data, CASE WHEN f.s_q3 THEN jsonb_build_object( 'target_change', s.col_q3_target_change, 'base_correction', s.col_q3_base_correction, 'base_correction_comment', s.col_q3_base_correction_comment, 'revision_inc', s.col_q3_revision_inc, 'revision_seq', s.col_q3_revision_seq, 'revision_comment', s.col_q3_revision_comment, 'new_plan', s.col_q3_new_plan, 'adj_current', s.col_q3_adj_current, 'adj_ssp', s.col_q3_adj_ssp, 'adj_rf', s.col_q3_adj_rf, 'adj_reserve', s.col_q3_adj_reserve, 'adj_comment', s.col_q3_adj_comment, 'corrected_plan', s.col_q3_corrected_plan, 'pay_date', s.col_q3_pay_date, 'pay_amount', s.col_q3_pay_amount, 'pay_ho', s.col_q3_pay_ho, 'pay_rf', s.col_q3_pay_rf, 'pay_comment', s.col_q3_pay_comment, 'pay_act', s.col_q3_pay_act, 'booking', s.col_q3_booking, 'actual_m1', s.col_q3_actual_m1, 'actual_m2', s.col_q3_actual_m2, 'actual_m3', s.col_q3_actual_m3, 'actual_quarter', s.col_q3_actual_quarter, 'residual_after_booking', s.col_q3_residual_after_booking, 'residual_after_actual', s.col_q3_residual_after_actual, 'transfer_q4', s.col_q3_transfer_q4, 'transfer_econ', s.col_q3_transfer_econ, 'total', s.col_q3_total ) END AS q3_data, CASE WHEN f.s_q4 THEN jsonb_build_object( 'target_change', s.col_q4_target_change, 'base_correction', s.col_q4_base_correction, 'base_correction_comment', s.col_q4_base_correction_comment, 'revision_inc', s.col_q4_revision_inc, 'revision_seq', s.col_q4_revision_seq, 'revision_comment', s.col_q4_revision_comment, 'new_plan', s.col_q4_new_plan, 'adj_current', s.col_q4_adj_current, 'adj_ssp', s.col_q4_adj_ssp, 'adj_rf', s.col_q4_adj_rf, 'adj_reserve', s.col_q4_adj_reserve, 'adj_comment', s.col_q4_adj_comment, 'corrected_plan', s.col_q4_corrected_plan, 'pay_date', s.col_q4_pay_date, 'pay_amount', s.col_q4_pay_amount, 'pay_ho', s.col_q4_pay_ho, 'pay_rf', s.col_q4_pay_rf, 'pay_comment', s.col_q4_pay_comment, 'pay_act', s.col_q4_pay_act, 'booking', s.col_q4_booking, 'actual_m1', s.col_q4_actual_m1, 'actual_m2', s.col_q4_actual_m2, 'actual_m3', s.col_q4_actual_m3, 'actual_spod', s.col_q4_actual_spod, 'actual_quarter', s.col_q4_actual_quarter, 'residual_after_booking', s.col_q4_residual_after_booking, 'residual_after_actual', s.col_q4_residual_after_actual, 'transfer_econ', s.col_q4_transfer_econ, 'total', s.col_q4_total ) END AS q4_data, CASE WHEN f.s_tot THEN jsonb_build_object( 'fact_year', s.col_fact_year ) END AS totals_data, s._sort_path FROM v3.v_form4_sheet_sections(p_form_id, p_sheet, p_sections) s CROSS JOIN flags f ORDER BY s._sort_path; $function$ ; -- DROP FUNCTION v3.v_form4_sheet_jsonb(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql STABLE AS $function$ -- 1. Базовые INPUT-чанки + project / section расширение WITH inp_raw AS ( SELECT j.* FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j WHERE j.row_type = 'INPUT' ), inp AS ( SELECT i.*, bl.project_id AS line_project_id, pp.id AS prj_id, pp.name AS prj_name, pg.id AS prog_id, pg.name AS prog_name, ei.id AS ei_id, ei.section_code, -- ROOT: ищем depth=0 предка через recursive CTE inline sec.id AS sec_id, sec.section_code AS sec_code, sec.name AS sec_name FROM inp_raw i JOIN v3.budget_line bl ON bl.id = i.line_id JOIN v3.expense_item ei ON ei.id = bl.expense_item_id LEFT JOIN v3.project pp ON pp.id = bl.project_id AND pp.level = 'project' LEFT JOIN v3.project pg ON pg.id = pp.parent_id AND pg.level = 'program' LEFT JOIN LATERAL ( WITH RECURSIVE up AS ( SELECT id, parent_id, name, section_code, depth FROM v3.expense_item WHERE id = ei.id UNION ALL SELECT e.id, e.parent_id, e.name, e.section_code, e.depth FROM v3.expense_item e JOIN up u ON u.parent_id = e.id ) SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1 ) sec ON TRUE ), -- 2. PROJECT агрегаты prj AS ( SELECT sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name, prog_id, MAX(prog_name) AS prog_name, prj_id, MAX(prj_name) AS prj_name, v3.jsonb_sum(plan_data) AS plan_data, v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data, v3.jsonb_sum(approved_data) AS approved_data, v3.jsonb_sum(contract_summary_data) AS contract_summary_data, v3.jsonb_sum(allocation_data) AS allocation_data, v3.jsonb_sum(reserve_data) AS reserve_data, v3.jsonb_sum(collegial_data) AS collegial_data, v3.jsonb_sum(ckk_data) AS ckk_data, v3.jsonb_sum(contract_data) AS contract_data, v3.jsonb_sum(booking_data) AS booking_data, v3.jsonb_sum(q1_data) AS q1_data, v3.jsonb_sum(q2_data) AS q2_data, v3.jsonb_sum(q3_data) AS q3_data, v3.jsonb_sum(q4_data) AS q4_data, v3.jsonb_sum(totals_data) AS totals_data FROM inp WHERE prj_id IS NOT NULL GROUP BY sec_id, prog_id, prj_id ), -- 3. PROGRAM агрегаты prog AS ( SELECT sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name, prog_id, MAX(prog_name) AS prog_name, v3.jsonb_sum(plan_data) AS plan_data, v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data, v3.jsonb_sum(approved_data) AS approved_data, v3.jsonb_sum(contract_summary_data) AS contract_summary_data, v3.jsonb_sum(allocation_data) AS allocation_data, v3.jsonb_sum(reserve_data) AS reserve_data, v3.jsonb_sum(collegial_data) AS collegial_data, v3.jsonb_sum(ckk_data) AS ckk_data, v3.jsonb_sum(contract_data) AS contract_data, v3.jsonb_sum(booking_data) AS booking_data, v3.jsonb_sum(q1_data) AS q1_data, v3.jsonb_sum(q2_data) AS q2_data, v3.jsonb_sum(q3_data) AS q3_data, v3.jsonb_sum(q4_data) AS q4_data, v3.jsonb_sum(totals_data) AS totals_data FROM inp WHERE prog_id IS NOT NULL GROUP BY sec_id, prog_id ), -- 4. ROOT (section) агрегаты по всем строкам секции (с проектом и без) root AS ( SELECT sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name, v3.jsonb_sum(plan_data) AS plan_data, v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data, v3.jsonb_sum(approved_data) AS approved_data, v3.jsonb_sum(contract_summary_data) AS contract_summary_data, v3.jsonb_sum(allocation_data) AS allocation_data, v3.jsonb_sum(reserve_data) AS reserve_data, v3.jsonb_sum(collegial_data) AS collegial_data, v3.jsonb_sum(ckk_data) AS ckk_data, v3.jsonb_sum(contract_data) AS contract_data, v3.jsonb_sum(booking_data) AS booking_data, v3.jsonb_sum(q1_data) AS q1_data, v3.jsonb_sum(q2_data) AS q2_data, v3.jsonb_sum(q3_data) AS q3_data, v3.jsonb_sum(q4_data) AS q4_data, v3.jsonb_sum(totals_data) AS totals_data FROM inp GROUP BY sec_id ), -- 5. Все 4 уровня в едином формате (sort_path для упорядочения) unioned AS ( SELECT 'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id, jsonb_build_object('section_code', sec_code, 'name', sec_name) AS header, plan_data, seq_dfip_data, approved_data, contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, contract_data, booking_data, q1_data, q2_data, q3_data, q4_data, totals_data, ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path FROM root UNION ALL SELECT 'GROUP', 1, NULL, jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id), plan_data, seq_dfip_data, approved_data, contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, contract_data, booking_data, q1_data, q2_data, q3_data, q4_data, totals_data, ARRAY[sec_id, prog_id, 0, 0]::INT[] FROM prog UNION ALL SELECT 'ITEM', 2, NULL, jsonb_build_object('section_code', sec_code, 'name', prj_name, 'program_id', prog_id, 'project_id', prj_id), plan_data, seq_dfip_data, approved_data, contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, contract_data, booking_data, q1_data, q2_data, q3_data, q4_data, totals_data, ARRAY[sec_id, prog_id, prj_id, 0]::INT[] FROM prj UNION ALL SELECT 'INPUT', 3, line_id, header || jsonb_build_object( 'project_id', prj_id, 'project_name', prj_name, 'program_id', prog_id, 'program_name', prog_name ), plan_data, seq_dfip_data, approved_data, contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, contract_data, booking_data, q1_data, q2_data, q3_data, q4_data, totals_data, ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(prj_id, 999999), line_id]::INT[] FROM inp ) SELECT u.row_type, u.depth, ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order, jsonb_build_object( 'line_id', u.line_id, 'header', u.header, 'plan', u.plan_data, 'seq_dfip', u.seq_dfip_data, 'approved', u.approved_data, 'contract_summary', u.contract_summary_data, 'allocation', u.allocation_data, 'reserve', u.reserve_data, 'collegial', u.collegial_data, 'ckk', u.ckk_data, 'contract', u.contract_data, 'booking', u.booking_data, 'q1', u.q1_data, 'q2', u.q2_data, 'q3', u.q3_data, 'q4', u.q4_data, 'totals', u.totals_data ) AS data FROM unioned u ORDER BY u.sort_path; $function$ ; -- DROP FUNCTION v3.v_form4_smeta(int4); CREATE OR REPLACE FUNCTION v3.v_form4_smeta(p_form_id integer) RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, plan_supp_q1 numeric, plan_supp_q2 numeric, plan_supp_q3 numeric, plan_supp_q4 numeric, plan_supp_year numeric, plan_dev_q1 numeric, plan_dev_q2 numeric, plan_dev_q3 numeric, plan_dev_q4 numeric, plan_dev_year numeric, appr_supp_q1 numeric, appr_supp_q2 numeric, appr_supp_q3 numeric, appr_supp_q4 numeric, appr_supp_year numeric, appr_dev_q1 numeric, appr_dev_q2 numeric, appr_dev_q3 numeric, appr_dev_q4 numeric, appr_dev_year numeric, fact_supp_q1 numeric, fact_supp_q2 numeric, fact_supp_q3 numeric, fact_supp_q4 numeric, fact_supp_year numeric, fact_dev_q1 numeric, fact_dev_q2 numeric, fact_dev_q3 numeric, fact_dev_q4 numeric, fact_dev_year numeric, corr_supp_q1 numeric, corr_supp_q2 numeric, corr_supp_q3 numeric, corr_supp_q4 numeric, corr_supp_year numeric, corr_dev_q1 numeric, corr_dev_q2 numeric, corr_dev_q3 numeric, corr_dev_q4 numeric, corr_dev_year numeric) LANGUAGE plpgsql STABLE AS $function$ #variable_conflict use_column BEGIN RETURN QUERY WITH -- Per-line per-direction агрегаты квартальных данных q_per_line AS ( SELECT line_id, SUM(CASE WHEN quarter=1 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq1, SUM(CASE WHEN quarter=2 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq2, SUM(CASE WHEN quarter=3 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq3, SUM(CASE WHEN quarter=4 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0)+COALESCE(actual_spod,0) END) AS fq4, SUM(CASE WHEN quarter=1 THEN transfer_to_q2 END) AS t1q2, SUM(CASE WHEN quarter=1 THEN transfer_to_q3 END) AS t1q3, SUM(CASE WHEN quarter=1 THEN transfer_to_q4 END) AS t1q4, SUM(CASE WHEN quarter=2 THEN transfer_to_q3 END) AS t2q3, SUM(CASE WHEN quarter=2 THEN transfer_to_q4 END) AS t2q4, SUM(CASE WHEN quarter=3 THEN transfer_to_q4 END) AS t3q4, SUM(CASE WHEN quarter=1 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj1, SUM(CASE WHEN quarter=2 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj2, SUM(CASE WHEN quarter=3 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj3, SUM(CASE WHEN quarter=4 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj4, -- Δ approved → new_plan: revision_inc/seq + target_change + base_correction -- (согласовано с v_form4_sheet_sections / Excel DJ10 = SUM(AN, CY, DH, CF, DE:DG)) SUM(CASE WHEN quarter=2 THEN COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0)+COALESCE(target_change,0)+COALESCE(base_plan_correction,0) END) AS rev2, SUM(CASE WHEN quarter=3 THEN COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0)+COALESCE(target_change,0)+COALESCE(base_plan_correction,0) END) AS rev3, SUM(CASE WHEN quarter=4 THEN COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0)+COALESCE(target_change,0)+COALESCE(base_plan_correction,0) END) AS rev4 FROM v3.budget_line_quarter GROUP BY line_id ), seq_dfip AS ( SELECT line_id, SUM(adj_q1) AS s1, SUM(adj_q2) AS s2, SUM(adj_q3) AS s3, SUM(adj_q4) AS s4 FROM v3.sequestration WHERE actor='DFIP' GROUP BY line_id ), seq_ssp AS ( SELECT line_id, SUM(adj_q1) AS g1, SUM(adj_q2) AS g2, SUM(adj_q3) AS g3, SUM(adj_q4) AS g4 FROM v3.sequestration WHERE actor='SSP_GO' GROUP BY line_id ), -- Per-line aggregate. -- FORM_4 = "Расходы на развитие" (АХР_Р!F5='ПРОЕКТНАЯ ДЕЯТЕЛЬНОСТЬ' в Excel), -- поэтому все строки идут в колонку Development независимо от -- expense_item.direction (этот справочник общий с FORM_2 и для FORM_2 -- проставлен 'Support'). Колонка Support по форме 4 всегда = 0. line_agg AS ( SELECT bl.expense_item_id AS eid, 'Development'::VARCHAR AS dir, SUM(p.plan_q1) AS pq1, SUM(p.plan_q2) AS pq2, SUM(p.plan_q3) AS pq3, SUM(p.plan_q4) AS pq4, SUM(sd.s1) AS sd1, SUM(sd.s2) AS sd2, SUM(sd.s3) AS sd3, SUM(sd.s4) AS sd4, SUM(sg.g1) AS sg1, SUM(sg.g2) AS sg2, SUM(sg.g3) AS sg3, SUM(sg.g4) AS sg4, SUM(r.amount_q1) AS rq1, SUM(r.amount_q2) AS rq2, SUM(r.amount_q3) AS rq3, SUM(r.amount_q4) AS rq4, SUM(qp.fq1) AS fq1, SUM(qp.fq2) AS fq2, SUM(qp.fq3) AS fq3, SUM(qp.fq4) AS fq4, SUM(qp.t1q2) AS t1q2, SUM(qp.t1q3) AS t1q3, SUM(qp.t1q4) AS t1q4, SUM(qp.t2q3) AS t2q3, SUM(qp.t2q4) AS t2q4, SUM(qp.t3q4) AS t3q4, SUM(qp.adj1) AS adj1, SUM(qp.adj2) AS adj2, SUM(qp.adj3) AS adj3, SUM(qp.adj4) AS adj4, SUM(qp.rev2) AS rev2, SUM(qp.rev3) AS rev3, SUM(qp.rev4) AS rev4 FROM v3.budget_line bl LEFT JOIN v3.plan p ON p.line_id = bl.id LEFT JOIN seq_dfip sd ON sd.line_id = bl.id LEFT JOIN seq_ssp sg ON sg.line_id = bl.id LEFT JOIN v3.reserve r ON r.line_id = bl.id LEFT JOIN q_per_line qp ON qp.line_id = bl.id WHERE bl.budget_form_id = p_form_id GROUP BY bl.expense_item_id ), -- Tree rollup с расщеплением по direction. Используем FILTER (WHERE) tw_rollup AS ( SELECT t.id, t.depth, t.item_id AS section_code, t.name, -- Поддержка SUM(a.pq1) FILTER (WHERE a.dir='Support') AS s_pq1, SUM(a.pq2) FILTER (WHERE a.dir='Support') AS s_pq2, SUM(a.pq3) FILTER (WHERE a.dir='Support') AS s_pq3, SUM(a.pq4) FILTER (WHERE a.dir='Support') AS s_pq4, SUM(a.sd1) FILTER (WHERE a.dir='Support') AS s_sd1, SUM(a.sd2) FILTER (WHERE a.dir='Support') AS s_sd2, SUM(a.sd3) FILTER (WHERE a.dir='Support') AS s_sd3, SUM(a.sd4) FILTER (WHERE a.dir='Support') AS s_sd4, SUM(a.sg1) FILTER (WHERE a.dir='Support') AS s_sg1, SUM(a.sg2) FILTER (WHERE a.dir='Support') AS s_sg2, SUM(a.sg3) FILTER (WHERE a.dir='Support') AS s_sg3, SUM(a.sg4) FILTER (WHERE a.dir='Support') AS s_sg4, SUM(a.rq1) FILTER (WHERE a.dir='Support') AS s_rq1, SUM(a.rq2) FILTER (WHERE a.dir='Support') AS s_rq2, SUM(a.rq3) FILTER (WHERE a.dir='Support') AS s_rq3, SUM(a.rq4) FILTER (WHERE a.dir='Support') AS s_rq4, SUM(a.fq1) FILTER (WHERE a.dir='Support') AS s_fq1, SUM(a.fq2) FILTER (WHERE a.dir='Support') AS s_fq2, SUM(a.fq3) FILTER (WHERE a.dir='Support') AS s_fq3, SUM(a.fq4) FILTER (WHERE a.dir='Support') AS s_fq4, SUM(a.t1q2) FILTER (WHERE a.dir='Support') AS s_t1q2, SUM(a.t1q3) FILTER (WHERE a.dir='Support') AS s_t1q3, SUM(a.t1q4) FILTER (WHERE a.dir='Support') AS s_t1q4, SUM(a.t2q3) FILTER (WHERE a.dir='Support') AS s_t2q3, SUM(a.t2q4) FILTER (WHERE a.dir='Support') AS s_t2q4, SUM(a.t3q4) FILTER (WHERE a.dir='Support') AS s_t3q4, SUM(a.adj1) FILTER (WHERE a.dir='Support') AS s_adj1, SUM(a.adj2) FILTER (WHERE a.dir='Support') AS s_adj2, SUM(a.adj3) FILTER (WHERE a.dir='Support') AS s_adj3, SUM(a.adj4) FILTER (WHERE a.dir='Support') AS s_adj4, SUM(a.rev2) FILTER (WHERE a.dir='Support') AS s_rev2, SUM(a.rev3) FILTER (WHERE a.dir='Support') AS s_rev3, SUM(a.rev4) FILTER (WHERE a.dir='Support') AS s_rev4, -- Развитие SUM(a.pq1) FILTER (WHERE a.dir='Development') AS d_pq1, SUM(a.pq2) FILTER (WHERE a.dir='Development') AS d_pq2, SUM(a.pq3) FILTER (WHERE a.dir='Development') AS d_pq3, SUM(a.pq4) FILTER (WHERE a.dir='Development') AS d_pq4, SUM(a.sd1) FILTER (WHERE a.dir='Development') AS d_sd1, SUM(a.sd2) FILTER (WHERE a.dir='Development') AS d_sd2, SUM(a.sd3) FILTER (WHERE a.dir='Development') AS d_sd3, SUM(a.sd4) FILTER (WHERE a.dir='Development') AS d_sd4, SUM(a.sg1) FILTER (WHERE a.dir='Development') AS d_sg1, SUM(a.sg2) FILTER (WHERE a.dir='Development') AS d_sg2, SUM(a.sg3) FILTER (WHERE a.dir='Development') AS d_sg3, SUM(a.sg4) FILTER (WHERE a.dir='Development') AS d_sg4, SUM(a.rq1) FILTER (WHERE a.dir='Development') AS d_rq1, SUM(a.rq2) FILTER (WHERE a.dir='Development') AS d_rq2, SUM(a.rq3) FILTER (WHERE a.dir='Development') AS d_rq3, SUM(a.rq4) FILTER (WHERE a.dir='Development') AS d_rq4, SUM(a.fq1) FILTER (WHERE a.dir='Development') AS d_fq1, SUM(a.fq2) FILTER (WHERE a.dir='Development') AS d_fq2, SUM(a.fq3) FILTER (WHERE a.dir='Development') AS d_fq3, SUM(a.fq4) FILTER (WHERE a.dir='Development') AS d_fq4, SUM(a.t1q2) FILTER (WHERE a.dir='Development') AS d_t1q2, SUM(a.t1q3) FILTER (WHERE a.dir='Development') AS d_t1q3, SUM(a.t1q4) FILTER (WHERE a.dir='Development') AS d_t1q4, SUM(a.t2q3) FILTER (WHERE a.dir='Development') AS d_t2q3, SUM(a.t2q4) FILTER (WHERE a.dir='Development') AS d_t2q4, SUM(a.t3q4) FILTER (WHERE a.dir='Development') AS d_t3q4, SUM(a.adj1) FILTER (WHERE a.dir='Development') AS d_adj1, SUM(a.adj2) FILTER (WHERE a.dir='Development') AS d_adj2, SUM(a.adj3) FILTER (WHERE a.dir='Development') AS d_adj3, SUM(a.adj4) FILTER (WHERE a.dir='Development') AS d_adj4, SUM(a.rev2) FILTER (WHERE a.dir='Development') AS d_rev2, SUM(a.rev3) FILTER (WHERE a.dir='Development') AS d_rev3, SUM(a.rev4) FILTER (WHERE a.dir='Development') AS d_rev4 FROM v3.mv_expense_item_tree t JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code LEFT JOIN line_agg a ON a.eid = ANY(t.desc_ids) WHERE t.depth <= 2 GROUP BY t.id, t.depth, t.item_id, t.name ) SELECT CASE r.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' END::VARCHAR, r.depth, r.section_code::VARCHAR, r.name::VARCHAR, -- Plan supp / dev r.s_pq1, r.s_pq2, r.s_pq3, r.s_pq4, COALESCE(r.s_pq1,0)+COALESCE(r.s_pq2,0)+COALESCE(r.s_pq3,0)+COALESCE(r.s_pq4,0), r.d_pq1, r.d_pq2, r.d_pq3, r.d_pq4, COALESCE(r.d_pq1,0)+COALESCE(r.d_pq2,0)+COALESCE(r.d_pq3,0)+COALESCE(r.d_pq4,0), -- Approved supp = plan + seq_dfip + seq_ssp + reserve COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0), COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0), COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0), COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0), COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0) +COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0) +COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0) +COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0), -- Approved dev COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0), COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0), COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0), COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0), COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0) +COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0) +COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0) +COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0), -- Fact supp / dev r.s_fq1, r.s_fq2, r.s_fq3, r.s_fq4, COALESCE(r.s_fq1,0)+COALESCE(r.s_fq2,0)+COALESCE(r.s_fq3,0)+COALESCE(r.s_fq4,0), r.d_fq1, r.d_fq2, r.d_fq3, r.d_fq4, COALESCE(r.d_fq1,0)+COALESCE(r.d_fq2,0)+COALESCE(r.d_fq3,0)+COALESCE(r.d_fq4,0), -- Corrected supp = approved + transfers + revisions + adj COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0) +COALESCE(r.s_adj1,0), COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0) +COALESCE(r.s_t1q2,0)+COALESCE(r.s_rev2,0)+COALESCE(r.s_adj2,0), COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0) +COALESCE(r.s_t1q3,0)+COALESCE(r.s_t2q3,0)+COALESCE(r.s_rev3,0)+COALESCE(r.s_adj3,0), COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0) +COALESCE(r.s_t1q4,0)+COALESCE(r.s_t2q4,0)+COALESCE(r.s_t3q4,0)+COALESCE(r.s_rev4,0)+COALESCE(r.s_adj4,0), COALESCE(r.s_pq1,0)+COALESCE(r.s_pq2,0)+COALESCE(r.s_pq3,0)+COALESCE(r.s_pq4,0) +COALESCE(r.s_sd1,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sd4,0) +COALESCE(r.s_sg1,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_sg4,0) +COALESCE(r.s_rq1,0)+COALESCE(r.s_rq2,0)+COALESCE(r.s_rq3,0)+COALESCE(r.s_rq4,0) +COALESCE(r.s_adj1,0)+COALESCE(r.s_adj2,0)+COALESCE(r.s_adj3,0)+COALESCE(r.s_adj4,0) +COALESCE(r.s_rev2,0)+COALESCE(r.s_rev3,0)+COALESCE(r.s_rev4,0), -- Corrected dev COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0) +COALESCE(r.d_adj1,0), COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0) +COALESCE(r.d_t1q2,0)+COALESCE(r.d_rev2,0)+COALESCE(r.d_adj2,0), COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0) +COALESCE(r.d_t1q3,0)+COALESCE(r.d_t2q3,0)+COALESCE(r.d_rev3,0)+COALESCE(r.d_adj3,0), COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0) +COALESCE(r.d_t1q4,0)+COALESCE(r.d_t2q4,0)+COALESCE(r.d_t3q4,0)+COALESCE(r.d_rev4,0)+COALESCE(r.d_adj4,0), COALESCE(r.d_pq1,0)+COALESCE(r.d_pq2,0)+COALESCE(r.d_pq3,0)+COALESCE(r.d_pq4,0) +COALESCE(r.d_sd1,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sd4,0) +COALESCE(r.d_sg1,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_sg4,0) +COALESCE(r.d_rq1,0)+COALESCE(r.d_rq2,0)+COALESCE(r.d_rq3,0)+COALESCE(r.d_rq4,0) +COALESCE(r.d_adj1,0)+COALESCE(r.d_adj2,0)+COALESCE(r.d_adj3,0)+COALESCE(r.d_adj4,0) +COALESCE(r.d_rev2,0)+COALESCE(r.d_rev3,0)+COALESCE(r.d_rev4,0) FROM tw_rollup r ORDER BY r.section_code; END; $function$ ; -- DROP FUNCTION v3.v_form4_view(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form4_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql STABLE AS $function$ BEGIN -- ═══ AHR / CAP / OPER — с проектной иерархией ════════════════════════════ IF p_sheet IN ('AHR','CAP','OPER') THEN RETURN QUERY SELECT * FROM v3.v_form4_sheet_jsonb(p_form_id, p_sheet, p_sections); RETURN; END IF; -- ═══ Структура_Р ═══════════════════════════════════════════════════════ -- AHR + CAP + OPER в одной выдаче, отсортировано по (section_code, -- num_group, _sort_path). БЕЗ проектной иерархии. IF p_sheet = 'STRUCTURE' THEN RETURN QUERY WITH all_sheets AS ( SELECT * FROM v3.v_form4_sheet_lines_jsonb(p_form_id, 'AHR', p_sections) UNION ALL SELECT * FROM v3.v_form4_sheet_lines_jsonb(p_form_id, 'CAP', p_sections) UNION ALL SELECT * FROM v3.v_form4_sheet_lines_jsonb(p_form_id, 'OPER', p_sections) ) SELECT j.row_type, j.depth, ROW_NUMBER() OVER ( ORDER BY (j.header->>'section_code'), (j.header->>'num_group') NULLS FIRST, j._sort_path ) AS sort_order, jsonb_build_object( 'line_id', j.line_id, 'header', j.header, 'plan', j.plan_data, 'seq_dfip', j.seq_dfip_data, 'approved', j.approved_data, 'contract_summary', j.contract_summary_data, 'allocation', j.allocation_data, 'reserve', j.reserve_data, 'collegial', j.collegial_data, 'ckk', j.ckk_data, 'contract', j.contract_data, 'booking', j.booking_data, 'q1', j.q1_data, 'q2', j.q2_data, 'q3', j.q3_data, 'q4', j.q4_data, 'totals', j.totals_data ) FROM all_sheets j ORDER BY (j.header->>'section_code'), (j.header->>'num_group') NULLS FIRST, j._sort_path; RETURN; END IF; -- ═══ Смета_2026 (с разделением Поддержка/Развитие) ═════════════════════ IF p_sheet = 'SMETA' THEN RETURN QUERY SELECT sm.row_type, sm.depth, ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, jsonb_build_object( 'section_code', sm.section_code, 'name', sm.name, 'plan', jsonb_build_object( 'support', jsonb_build_object('q1', sm.plan_supp_q1, 'q2', sm.plan_supp_q2, 'q3', sm.plan_supp_q3, 'q4', sm.plan_supp_q4, 'year', sm.plan_supp_year), 'development', jsonb_build_object('q1', sm.plan_dev_q1, 'q2', sm.plan_dev_q2, 'q3', sm.plan_dev_q3, 'q4', sm.plan_dev_q4, 'year', sm.plan_dev_year) ), 'approved', jsonb_build_object( 'support', jsonb_build_object('q1', sm.appr_supp_q1, 'q2', sm.appr_supp_q2, 'q3', sm.appr_supp_q3, 'q4', sm.appr_supp_q4, 'year', sm.appr_supp_year), 'development', jsonb_build_object('q1', sm.appr_dev_q1, 'q2', sm.appr_dev_q2, 'q3', sm.appr_dev_q3, 'q4', sm.appr_dev_q4, 'year', sm.appr_dev_year) ), 'fact', jsonb_build_object( 'support', jsonb_build_object('q1', sm.fact_supp_q1, 'q2', sm.fact_supp_q2, 'q3', sm.fact_supp_q3, 'q4', sm.fact_supp_q4, 'year', sm.fact_supp_year), 'development', jsonb_build_object('q1', sm.fact_dev_q1, 'q2', sm.fact_dev_q2, 'q3', sm.fact_dev_q3, 'q4', sm.fact_dev_q4, 'year', sm.fact_dev_year) ), 'corrected', jsonb_build_object( 'support', jsonb_build_object('q1', sm.corr_supp_q1, 'q2', sm.corr_supp_q2, 'q3', sm.corr_supp_q3, 'q4', sm.corr_supp_q4, 'year', sm.corr_supp_year), 'development', jsonb_build_object('q1', sm.corr_dev_q1, 'q2', sm.corr_dev_q2, 'q3', sm.corr_dev_q3, 'q4', sm.corr_dev_q4, 'year', sm.corr_dev_year) ) ) FROM v3.v_form4_smeta(p_form_id) sm ORDER BY sm.section_code; RETURN; END IF; RAISE EXCEPTION 'Unknown sheet for FORM_4: %', p_sheet USING HINT = 'Use AHR / CAP / OPER / SMETA / STRUCTURE'; END; $function$ ; -- DROP FUNCTION v3.v_form_view(int4, varchar, _text, varchar); CREATE OR REPLACE FUNCTION v3.v_form_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_direction character varying DEFAULT NULL::character varying) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql STABLE AS $function$ DECLARE v_form_type VARCHAR; v_year INT; v_org_unit_id INT; BEGIN -- Определяем form_type автоматически SELECT form_type_code, year, org_unit_id INTO v_form_type, v_year, v_org_unit_id FROM v3.budget_form WHERE id = p_form_id; IF NOT FOUND THEN RAISE EXCEPTION 'budget_form id=% не существует в v3.budget_form', p_form_id; END IF; -- ═══ FORM_1 ═══════════════════════════════════════════════════════════════ IF v_form_type = 'FORM_1' THEN IF p_sheet = 'SMETA' THEN -- v3.v_form1_smeta параметризуется year+org_unit_id RETURN QUERY SELECT sm.row_type, sm.depth, ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, jsonb_build_object( 'section_code', sm.section_code, 'name', sm.name, 'plan', jsonb_build_object( 'support', jsonb_build_object('q1', sm.supp_plan_q1, 'q2', sm.supp_plan_q2, 'q3', sm.supp_plan_q3, 'q4', sm.supp_plan_q4, 'year', sm.supp_plan_year), 'development', jsonb_build_object('q1', sm.dev_plan_q1, 'q2', sm.dev_plan_q2, 'q3', sm.dev_plan_q3, 'q4', sm.dev_plan_q4, 'year', sm.dev_plan_year), 'total_year', sm.total_plan_year), 'approved', jsonb_build_object( 'support', jsonb_build_object('q1', sm.supp_appr_q1, 'q2', sm.supp_appr_q2, 'q3', sm.supp_appr_q3, 'q4', sm.supp_appr_q4, 'year', sm.supp_appr_year), 'development', jsonb_build_object('q1', sm.dev_appr_q1, 'q2', sm.dev_appr_q2, 'q3', sm.dev_appr_q3, 'q4', sm.dev_appr_q4, 'year', sm.dev_appr_year), 'total_year', sm.total_appr_year), 'fact', jsonb_build_object( 'support', jsonb_build_object('q1', sm.supp_act_q1, 'q2', sm.supp_act_q2, 'q3', sm.supp_act_q3, 'q4', sm.supp_act_q4, 'year', sm.supp_act_year), 'development', jsonb_build_object('q1', sm.dev_act_q1, 'q2', sm.dev_act_q2, 'q3', sm.dev_act_q3, 'q4', sm.dev_act_q4, 'year', sm.dev_act_year), 'total_year', sm.total_act_year), 'corrected', jsonb_build_object( 'support', jsonb_build_object('q2', sm.supp_corr_q2, 'q3', sm.supp_corr_q3, 'q4', sm.supp_corr_q4), 'development', jsonb_build_object('q2', sm.dev_corr_q2, 'q3', sm.dev_corr_q3, 'q4', sm.dev_corr_q4)) ) FROM v3.v_form1_smeta(v_year, v_org_unit_id) sm ORDER BY sm.section_code; RETURN; END IF; IF p_sheet IN ('AHR','CAP','OPER') THEN RETURN QUERY SELECT j.row_type, j.depth, ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order, jsonb_build_object( 'line_id', j.line_id, 'header', j.header, 'plan', j.plan_data, 'contract_summary', j.contract_summary, 'allocation', j.allocation_data, 'sequestration', j.sequestration_data, 'reserve', j.reserve_data, 'approved', j.approved_data, 'collegial', j.collegial_data, 'ckk', j.ckk_data, 'contract_detail', j.contract_detail, 'q1', j.q1_data, 'q2', j.q2_data, 'q3', j.q3_data, 'q4', j.q4_data, 'totals', j.totals_data ) FROM v3.v_form1_sheet_jsonb(p_form_id, p_sheet, p_direction, p_sections) j ORDER BY j._sort_path; RETURN; END IF; RAISE EXCEPTION 'Unknown FORM_1 sheet: %', p_sheet USING HINT = 'Use AHR / CAP / OPER / SMETA'; END IF; -- ═══ FORM_2 ═══════════════════════════════════════════════════════════════ IF v_form_type = 'FORM_2' THEN RETURN QUERY SELECT * FROM v3.v_form2_view(p_form_id, p_sheet, p_sections); RETURN; END IF; -- ═══ FORM_4 ═══════════════════════════════════════════════════════════════ IF v_form_type = 'FORM_4' THEN RETURN QUERY SELECT * FROM v3.v_form4_view(p_form_id, p_sheet, p_sections); RETURN; END IF; RAISE EXCEPTION 'Unsupported form_type=% for form_id=%', v_form_type, p_form_id; END; $function$ ; CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id INT) RETURNS INTEGER AS $function$ DECLARE v_report_type VARCHAR; v_inserted INTEGER; BEGIN SELECT report_type INTO v_report_type FROM v3.rf_project_report WHERE id = p_report_id; IF NOT FOUND THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; INSERT INTO v3.form3_phase (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at) SELECT p_report_id, pt.phase_code, pt.role, pt.column_keys, pt.opens_at, pt.closes_at FROM v3.phase_template pt WHERE pt.form_type = 'FORM_3' AND pt.sheet = v_report_type ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING; GET DIAGNOSTICS v_inserted = ROW_COUNT; RETURN v_inserted; END; $function$ LANGUAGE plpgsql VOLATILE; CREATE OR REPLACE FUNCTION v3.user_in_report_org(p_user_id INT, p_report_id INT) RETURNS BOOLEAN AS $function$ DECLARE v_org INT; BEGIN SELECT p.org_unit_id INTO v_org FROM v3.rf_project_report r JOIN v3.project p ON p.id = r.project_id WHERE r.id = p_report_id; IF v_org IS NULL THEN RETURN TRUE; -- проект без org_unit — пропускаем фильтр END IF; RETURN EXISTS ( SELECT 1 FROM v3.user_org WHERE user_id = p_user_id AND org_unit_id = v_org ); END; $function$ LANGUAGE plpgsql STABLE; CREATE OR REPLACE FUNCTION v3.editable_columns_for3( p_report_id INT, p_user_id INT ) RETURNS TABLE ( column_key TEXT, closes_at TIMESTAMPTZ ) AS $function$ DECLARE v_role VARCHAR; BEGIN IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; v_role := v3.user_role_code(p_user_id); IF v_role IS NULL THEN RETURN; -- неизвестный/неактивный юзер → пустая маска END IF; -- ADMIN: UNION всех column_keys всех фаз отчёта. IF v_role = 'ADMIN' THEN RETURN QUERY SELECT ck AS column_key, MAX(fp.closes_at) AS closes_at FROM v3.form3_phase fp, LATERAL unnest(fp.column_keys) AS ck WHERE fp.rf_project_report_id = p_report_id GROUP BY ck; RETURN; END IF; -- EXECUTOR_RF без назначения → пустая маска (default deny). IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN RETURN; END IF; RETURN QUERY SELECT ck AS column_key, MAX(fp.closes_at) AS closes_at FROM v3.form3_phase fp, LATERAL unnest(fp.column_keys) AS ck WHERE fp.rf_project_report_id = p_report_id AND fp.role = v_role AND now() BETWEEN fp.opens_at AND fp.closes_at GROUP BY ck; END; $function$ LANGUAGE plpgsql STABLE; CREATE OR REPLACE FUNCTION v3.can_edit3( p_report_id INT, p_column_key TEXT, p_user_id INT ) RETURNS JSONB AS $function$ DECLARE v_role VARCHAR; v_active_close TIMESTAMPTZ; v_role_has_col BOOLEAN; v_next_open TIMESTAMPTZ; v_last_close TIMESTAMPTZ; BEGIN IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; v_role := v3.user_role_code(p_user_id); IF v_role IS NULL THEN RETURN jsonb_build_object( 'ok', false, 'code', 'role_not_allowed', 'detail', 'unknown or inactive user' ); END IF; -- ADMIN — bypass всех проверок. IF v_role = 'ADMIN' THEN RETURN jsonb_build_object('ok', true, 'admin', true); END IF; -- EXECUTOR_RF: фильтр по user_org проекта отчёта. IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN RETURN jsonb_build_object( 'ok', false, 'code', 'org_not_assigned', 'detail', p_column_key ); END IF; -- 1. Активная сейчас фаза, в которой эта колонка перечислена для этой роли? SELECT MAX(fp.closes_at) INTO v_active_close FROM v3.form3_phase fp WHERE fp.rf_project_report_id = p_report_id AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) AND now() BETWEEN fp.opens_at AND fp.closes_at; IF v_active_close IS NOT NULL THEN RETURN jsonb_build_object( 'ok', true, 'closes_at', v_active_close ); END IF; -- 2. Колонка В ПРИНЦИПЕ есть хоть в одной фазе этой роли на этом отчёте? SELECT EXISTS ( SELECT 1 FROM v3.form3_phase fp WHERE fp.rf_project_report_id = p_report_id AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) ) INTO v_role_has_col; IF NOT v_role_has_col THEN RETURN jsonb_build_object( 'ok', false, 'code', 'role_not_allowed', 'detail', p_column_key ); END IF; -- 3. Колонка доступна, но окно не активно. Подсказать ближайшее. SELECT MIN(fp.opens_at) INTO v_next_open FROM v3.form3_phase fp WHERE fp.rf_project_report_id = p_report_id AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) AND fp.opens_at > now(); IF v_next_open IS NOT NULL THEN RETURN jsonb_build_object( 'ok', false, 'code', 'window_closed', 'detail', p_column_key, 'opens_at', v_next_open ); END IF; SELECT MAX(fp.closes_at) INTO v_last_close FROM v3.form3_phase fp WHERE fp.rf_project_report_id = p_report_id AND fp.role = v_role AND p_column_key = ANY(fp.column_keys) AND fp.closes_at <= now(); RETURN jsonb_build_object( 'ok', false, 'code', 'window_closed', 'detail', p_column_key, 'closes_at', v_last_close ); END; $function$ LANGUAGE plpgsql STABLE; -- ─── 3. Authz-обёртки upd_form3_cell/cells (доп. сигнатуры с p_user_id) ───── CREATE OR REPLACE FUNCTION v3.upd_form3_cells( p_report_id INT, p_changes JSONB, p_user_id INT ) RETURNS TABLE ( row_type VARCHAR, depth INT, sort_order BIGINT, data JSONB ) AS $function$ DECLARE v_change JSONB; v_column TEXT; v_decision JSONB; v_msg TEXT; BEGIN IF p_user_id IS NULL THEN RAISE EXCEPTION 'role_not_allowed: '; END IF; IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id; END IF; IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN RAISE EXCEPTION 'p_changes must be a JSONB array'; END IF; FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes) LOOP v_column := v_change->>'column'; IF v_column IS NULL THEN RAISE EXCEPTION 'change must have column: %', v_change; END IF; v_decision := v3.can_edit3(p_report_id, v_column, p_user_id); IF NOT (v_decision->>'ok')::boolean THEN IF v_decision->>'code' = 'role_not_allowed' THEN RAISE EXCEPTION 'role_not_allowed: %', v_column; ELSIF v_decision->>'code' = 'org_not_assigned' THEN RAISE EXCEPTION 'org_not_assigned: %', v_column; ELSIF v_decision->>'code' = 'window_closed' THEN IF v_decision ? 'opens_at' THEN v_msg := format('window_closed: %s (opens %s)', v_column, v_decision->>'opens_at'); ELSIF v_decision->>'closes_at' IS NOT NULL THEN v_msg := format('window_closed: %s (closed %s)', v_column, v_decision->>'closes_at'); ELSE v_msg := format('window_closed: %s (no phase)', v_column); END IF; RAISE EXCEPTION '%', v_msg; ELSE RAISE EXCEPTION '%: %', v_decision->>'code', v_column; END IF; END IF; END LOOP; RETURN QUERY SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes); END; $function$ LANGUAGE plpgsql VOLATILE; CREATE OR REPLACE FUNCTION v3.upd_form3_cell( p_report_id INT, p_line_id INT, p_column TEXT, p_value JSONB, p_user_id INT ) RETURNS TABLE ( row_type VARCHAR, depth INT, sort_order BIGINT, data JSONB ) AS $function$ SELECT * FROM v3.upd_form3_cells( p_report_id, jsonb_build_array(jsonb_build_object( 'line_id', p_line_id, 'column', p_column, 'value', p_value )), p_user_id ); $function$ LANGUAGE sql VOLATILE; -- ─── 4. add_project — раскладка snapshot обоих отчётов ────────────────────── CREATE OR REPLACE FUNCTION v3.add_project( p_name VARCHAR, p_year INT, p_org_unit_id INT, p_level VARCHAR DEFAULT 'project', p_parent_id INT DEFAULT NULL, p_project_type VARCHAR DEFAULT NULL, p_vsp_format VARCHAR DEFAULT NULL, p_placement_type VARCHAR DEFAULT NULL, p_object_address VARCHAR DEFAULT NULL, p_staff_count INT DEFAULT NULL, p_total_area NUMERIC DEFAULT NULL ) RETURNS TABLE ( project_id INT, limit_report_id INT, current_expenses_report_id INT ) AS $function$ DECLARE v_pid INT; v_lim INT; v_cur INT; 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 WHERE id = p_org_unit_id) 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; INSERT INTO v3.project ( name, level, parent_id, org_unit_id, project_type, vsp_format, placement_type, object_address, staff_count, total_area ) 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 ) RETURNING id INTO v_pid; INSERT INTO v3.rf_project_report (project_id, year, report_type) VALUES (v_pid, p_year, 'LIMIT') RETURNING id INTO v_lim; INSERT INTO v3.rf_project_report (project_id, year, report_type) VALUES (v_pid, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur; -- Snapshot RBAC-фаз (иначе default deny — отчёт read-only). PERFORM v3.copy_template_to_report(v_lim); PERFORM v3.copy_template_to_report(v_cur); PERFORM v3.log_event( 'PROJECT_CREATE', 'PROJECT_CREATE', 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, '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$ LANGUAGE plpgsql VOLATILE;