2465 lines
129 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

-- удаление проектов
CREATE OR REPLACE FUNCTION v3.can_delete_project(p_project_id integer)
RETURNS BOOL
LANGUAGE plpgsql
AS $function$
DECLARE
v_old RECORD;
v_actor_id INTEGER;
v_role VARCHAR;
-- v_phase v3.form3_phase;
BEGIN
v_actor_id := v3.current_user_id();
v_role := v3.user_role_code(v_actor_id);
SELECT p.name, p.level, p.org_unit_id, p.created_at
INTO v_old
FROM v3.project p WHERE p.id = p_project_id;
IF v_role != 'ADMIN' AND NOT EXISTS (
SELECT 1 FROM v3.user_org uo
WHERE uo.org_unit_id = v_old.org_unit_id AND uo.user_id = v_actor_id
) THEN
RETURN false;
END IF;
IF (
select count(*) from (
select rpr.id, max(fp.closes_at) from v3.form3_phase fp
inner join v3.rf_project_report rpr
on rpr.id = fp.rf_project_report_id
and rpr.project_id = p_project_id
and fp.opens_at <= current_timestamp
and current_timestamp <= fp.closes_at
and fp.opens_at <= v_old.created_at
and v_old.created_at <= fp.closes_at
group by rpr.id
)
) = (select COUNT(*) from v3.rf_project_report rpr where rpr.project_id = p_project_id) THEN
RETURN true;
END IF;
IF NOT EXISTS (
SELECT 1
FROM v3.rf_project_report_line rprl
INNER JOIN v3.rf_project_report rpr ON rpr.id = rprl.rf_project_report_id
WHERE rpr.project_id = p_project_id
) THEN
RETURN true;
END IF;
RETURN false;
END;
$function$
;
CREATE OR REPLACE FUNCTION v3.del_project(p_project_id integer)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
v_old RECORD;
v_actor_id INTEGER;
v_role VARCHAR;
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;
IF NOT v3.can_delete_project(p_project_id) THEN
RAISE EXCEPTION 'project #% невозможно удалить', p_project_id;
END IF;
-- Каскад вручную: rf_project_report_quarter / line / report (нет ON DELETE)
DELETE FROM v3.form3_phase f3p WHERE f3p.rf_project_report_id IN (
SELECT id FROM v3.rf_project_report rpr WHERE project_id = p_project_id
);
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$
;
-- Форма 4
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,
r.justification AS rjust,
-- 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, CASE WHEN s_rsv THEN f.rjust END,
-- 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$
;
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(
'total', 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_y1', s.col_cs_future_y1,
'future_y2', s.col_cs_future_y2,
'other_ssp', 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(
'approved', s.col_col_amount,
'protocol', 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,
'q1', s.col_ckk_q1,
'q2', s.col_ckk_q2,
'q3', s.col_ckk_q3,
'q4', s.col_ckk_q4,
'rf_schedule', s.col_ckk_rf_schedule,
'deadline', s.col_ckk_delivery_deadline,
'proc_plan', s.col_ckk_procurement_plan,
'proc_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,
'q1', s.col_cd_q1,
'q2', s.col_cd_q2,
'q3', s.col_cd_q3,
'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$
;
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-строки с расширенной иерархией ═══════════════════════
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,
-- --- разрешение проекта ---
-- A. bl.project_id → project (level='project')
-- B. bl.project_id → program (level='program') — тогда проекта нет
CASE WHEN prj.id IS NOT NULL THEN prj.id END AS prj_id,
CASE WHEN prj.id IS NOT NULL THEN prj.name END AS prj_name,
-- --- разрешение программы ---
-- A. через проект: prog_via_prj
-- B. напрямую: prog_direct
COALESCE(prog_via_prj.id, prog_direct.id) AS prog_id,
COALESCE(prog_via_prj.name, prog_direct.name) AS prog_name,
-- --- section (depth=0) ---
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
-- проект (level='project')
LEFT JOIN v3.form4_project prj
ON prj.id = bl.project_id AND prj.level = 'project'
-- программа через проект
LEFT JOIN v3.form4_project prog_via_prj
ON prog_via_prj.id = prj.parent_id AND prog_via_prj.level = 'program'
-- программа напрямую (bl.project_id → program)
LEFT JOIN v3.form4_project prog_direct
ON prog_direct.id = bl.project_id AND prog_direct.level = 'program'
-- раздел depth=0 (через expense_item)
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
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
),
tp_agg AS (
SELECT 1 AS tp_id,
jsonb_build_object(
'q1', 0, 'q2', 0,
'q3', 0, 'q4', 0,
'year', 0, 'comment', null
) AS tp_plan,
jsonb_build_object(
'q1', 0, 'q2', 0,
'q3', 0, 'q4', 0,
'year', 0,
'justification', null
) AS tp_seq_dfip,
jsonb_build_object(
'q1', 0, 'q2', 0,
'q3', 0, 'q4', 0,
'year', 0
) AS tp_approved,
jsonb_build_object(
'total', null,
'reference', null,
'counterparty', null,
'deadline', null,
'comment', null,
'future_y1', null,
'future_y2', null,
'other_ssp', null,
'centralized_flag', null
) AS tp_contract_summary,
jsonb_build_object(
'contract_ref', null,
'allocation_purpose', null
) AS tp_allocation,
jsonb_build_object(
'q1', 0, 'q2', 0,
'q3', 0, 'q4', 0,
'year', 0,
'justification', null
) AS tp_reserve,
jsonb_build_object(
'approved', null,
'protocol', null,
'note', null
) AS tp_collegial,
jsonb_build_object(
'ceiling', null,
'q1', null,
'q2', null,
'q3', null,
'q4', null,
'rf_schedule', null,
'delivery_deadline', null,
'proc_plan', null,
'proc_method', null,
'comment', null
) AS tp_ckk,
jsonb_build_object(
'act', null,
'date', null,
'scheme', null,
'addenda', null,
'ceiling', null,
'comment', null,
'subject', null,
'currency', null,
'deadline', null,
'vat_rate', null,
'reference', null,
'q1', null,
'q2', null,
'q3', null,
'q4', null,
'rf_schedule', null,
'counterparty', null,
'exchange_rate', null,
'amount_foreign', null
) AS tp_contract,
jsonb_build_object(
'y2026', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null),
'y2027', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null)
) AS tp_booking,
jsonb_build_object(
'total', 0,
'adj_rf', null,
'pay_ho', null,
'pay_rf', null,
'adj_ssp', null,
'booking', null,
'pay_act', null,
'pay_date', null,
'actual_m1', null,
'actual_m2', null,
'actual_m3', null,
'pay_amount', null,
'adj_comment', null,
'adj_current', null,
'adj_reserve', null,
'pay_comment', null,
'transfer_q2', null,
'transfer_q3', null,
'transfer_q4', null,
'transfer_econ', null,
'actual_quarter', 0,
'corrected_plan', 0,
'residual_after_actual', 0,
'residual_after_booking', 0
) AS tp_q1,
jsonb_build_object(
'target_change', null,
'base_correction', null,
'base_correction_comment', null,
'revision_inc', null,
'revision_seq', null,
'revision_comment', null,
'new_plan', 0,
'adj_current', null,
'adj_ssp', null,
'adj_rf', null,
'adj_reserve', null,
'adj_comment', null,
'corrected_plan', 0,
'pay_date', null,
'pay_amount', null,
'pay_ho', null,
'pay_rf', null,
'pay_comment', null,
'pay_act', null,
'booking', null,
'actual_m1', null,
'actual_m2', null,
'actual_m3', null,
'actual_quarter', 0,
'residual_after_booking', 0,
'residual_after_actual', 0,
'transfer_q3', null,
'transfer_q4', null,
'transfer_econ', null,
'total', 0
) AS tp_q2,
jsonb_build_object(
'target_change', null,
'base_correction', null,
'base_correction_comment', null,
'revision_inc', null,
'revision_seq', null,
'revision_comment', null,
'new_plan', 0,
'adj_current', null,
'adj_ssp', null,
'adj_rf', null,
'adj_reserve', null,
'adj_comment', null,
'corrected_plan', 0,
'pay_date', null,
'pay_amount', null,
'pay_ho', null,
'pay_rf', null,
'pay_comment', null,
'pay_act', null,
'booking', null,
'actual_m1', null,
'actual_m2', null,
'actual_m3', null,
'actual_quarter', 0,
'residual_after_booking', 0,
'residual_after_actual', 0,
'transfer_q4', null,
'transfer_econ', null,
'total', 0
) AS tp_q3,
jsonb_build_object(
'target_change', null,
'base_correction', null,
'base_correction_comment', null,
'revision_inc', null,
'revision_seq', null,
'revision_comment', null,
'new_plan', 0,
'adj_current', null,
'adj_ssp', null,
'adj_rf', null,
'adj_reserve', null,
'adj_comment', null,
'corrected_plan', 0,
'pay_date', null,
'pay_amount', null,
'pay_ho', null,
'pay_rf', null,
'pay_comment', null,
'pay_act', null,
'booking', null,
'actual_m1', null,
'actual_m2', null,
'actual_m3', null,
'actual_spod', null,
'actual_quarter', 0,
'residual_after_booking', 0,
'residual_after_actual', 0,
'transfer_econ', null,
'total', 0
) AS tp_q4,
jsonb_build_object(
'fact_year', 0
) AS tp_totals
),
-- ═══ 2. Все программы (включая пустые) ═════════════════════════════════════
all_programs AS (
SELECT
fp.id AS prog_id,
fp.name AS prog_name,
fp.section_code,
sec.id AS sec_id,
sec.section_code AS sec_code,
sec.name AS sec_name
FROM v3.form4_project fp
LEFT JOIN LATERAL (
SELECT id, section_code, name
FROM v3.expense_item
WHERE section_code = fp.section_code AND depth = 0
LIMIT 1
) sec ON TRUE
WHERE fp.level = 'program'
AND (p_sections IS NULL OR fp.section_code = ANY(p_sections))
AND (fp.section_code = p_sheet) AND fp.form_id = p_form_id
),
-- ═══ 3. Все проекты под программами (включая пустые) ═══════════════════════
all_projects AS (
SELECT
prj.id AS prj_id,
prj.name AS prj_name,
prj.parent_id AS prog_id,
ap.sec_id, ap.sec_code, ap.sec_name
FROM v3.form4_project prj
JOIN all_programs ap ON ap.prog_id = prj.parent_id
WHERE prj.level = 'project' AND (ap.section_code = p_sheet)
),
-- ═══ 4. Агрегаты с финансовыми данными (только из inp) ════════════════════
-- PROJECT-агрегаты
prj_agg 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
),
-- PROGRAM-агрегаты
prog_agg 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
),
-- ROOT-агрегаты
root_agg 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
),
-- Нулевой JSONB-шаблон (для пустых программ/проектов)
zero AS (
SELECT '{}'::jsonb AS z
),
-- ═══ 5. UNION всех уровней ═════════════════════════════════════════════════
unioned AS (
-- ROOT — только из агрегатов (не имеет смысла пустой ROOT без строк)
SELECT
'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id,
jsonb_build_object('section_code', sec_code, 'name', sec_name, 'expense_item_id', sec_id) 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_agg
UNION ALL
-- GROUP — из агрегатов (программы с бюджетными строками)
SELECT
'GROUP', 1, NULL,
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_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_agg
UNION ALL
-- GROUP — пустые программы (нет ни одной budget_line)
SELECT
'GROUP', 1, NULL,
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved
tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve
tp_collegial, tp_ckk, -- collegial, ckk
tp_contract, tp_booking, -- contract, booking
tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals
ARRAY[sec_id, prog_id, 0, 0]::INT[]
FROM all_programs ap
JOIN tp_agg ON tp_agg.tp_id = 1
WHERE NOT EXISTS (
SELECT 1 FROM v3.budget_line bl
WHERE bl.budget_form_id = p_form_id
AND (
bl.project_id = ap.prog_id -- прямая привязка к программе
OR bl.project_id IN (SELECT id FROM v3.form4_project pr -- через проект
WHERE pr.parent_id = ap.prog_id AND pr.level = 'project')
)
)
UNION ALL
-- ITEM — из агрегатов (проекты с бюджетными строками)
SELECT
'ITEM', 2, NULL,
jsonb_build_object('section_code', sec_code, 'name', prj_name,
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_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_agg
UNION ALL
-- ITEM — пустые проекты (нет budget_line)
SELECT
'ITEM', 2, NULL,
jsonb_build_object('section_code', sec_code, 'name', prj_name,
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved
tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve
tp_collegial, tp_ckk, -- collegial, ckk
tp_contract, tp_booking, -- contract, booking
tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
FROM all_projects apr
JOIN tp_agg ON tp_agg.tp_id = 1
WHERE NOT EXISTS (
SELECT 1 FROM v3.budget_line bl
WHERE bl.budget_form_id = p_form_id
AND bl.project_id = apr.prj_id
)
UNION ALL
-- INPUT — все строки бюджета
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(CASE WHEN prj_id is null and prog_id is not null THEN 0 ELSE prj_id END, 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(
'sort_path', u.sort_path,
'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$
;
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'),
('allocation','contract_ref', 'allocation','contract_ref','TEXT','line'),
('allocation','allocation_purpose', 'allocation','allocation_purpose','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'),
('contract_summary','reference', 'contract_summary','reference', 'TEXT','line'),
('contract_summary','centralized_flag', 'contract_summary','centralized_flag', 'TEXT','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','addenda', 'contract_detail','addenda', 'TEXT', 'line'),
('contract','amount_foreign', 'contract_detail','amount_foreign','NUMERIC','line'),
('contract','q1', 'contract_detail','expenses_q1', 'NUMERIC','line'),
('contract','q2', 'contract_detail','expenses_q2', 'NUMERIC','line'),
('contract','q3', 'contract_detail','expenses_q3', 'NUMERIC','line'),
('contract','q4', 'contract_detail','expenses_q4', 'NUMERIC','line'),
('contract','rf_schedule', 'contract_detail','rf_schedule', 'TEXT', 'line'),
('contract','exchange_rate', 'contract_detail','exchange_rate', 'NUMERIC','line'),
('contract','reference', 'contract_detail','reference', 'TEXT', 'line'),
('contract','date', 'contract_detail','contract_date', 'DATE', 'line'),
('contract','subject', 'contract_detail','subject', 'TEXT', 'line'),
('contract','currency', 'contract_detail','currency', 'TEXT', 'line'),
('contract','ceiling', 'contract_detail','ceiling_amount','NUMERIC','line'),
('contract','vat_rate', 'contract_detail','vat_rate', 'TEXT', 'line'),
('contract','deadline', 'contract_detail','deadline', 'TEXT', 'line'),
('contract','scheme', 'contract_detail','payment_scheme','TEXT', 'line'),
('contract','act', 'contract_detail','act', 'TEXT', 'line'),
('contract','comment', 'contract_detail','comment', 'TEXT', 'line'),
-- ─── sequestration (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$
;
-- Удаляем FK у audit_log на случай удаления исконной записи и сохранения в журнале
ALTER TABLE v3.audit_log DROP CONSTRAINT IF EXISTS audit_log_app_user_fk;
ALTER TABLE v3.audit_log DROP CONSTRAINT IF EXISTS audit_log_org_unit_fk;
ALTER TABLE v3.audit_log DROP CONSTRAINT IF EXISTS audit_log_budget_form_fk;
ALTER TABLE v3.project ADD COLUMN created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now();
-- ════════════════════════════════════════════════════════════════════════════
-- ПАТЧ: v3.v_form_view — добавление p_user_id + META-строки (editable-маска)
-- ════════════════════════════════════════════════════════════════════════════
-- Идемпотентен. Пересоздаёт функцию (DROP обеих сигнатур + CREATE).
-- Зависимости (должны уже быть на целевой БД): v3.editable_columns_for(INT,INT,VARCHAR),
-- v3.user_role_code(INT), v3.budget_form, а также v_form1/2/4-функции.
--
-- Применение:
-- psql -U dfip -d dfip -f patch_v_form_view_meta.sql
-- docker exec -i <container> psql -U dfip -d dfip < patch_v_form_view_meta.sql
-- ════════════════════════════════════════════════════════════════════════════
-- ─── precheck зависимостей: падаем ДО DROP, если чего-то нет ─────────────────
DO $precheck$
BEGIN
IF to_regprocedure('v3.editable_columns_for(int,int,varchar)') IS NULL THEN
RAISE EXCEPTION 'Зависимость отсутствует: v3.editable_columns_for(INT,INT,VARCHAR). Накатите sql/v3/permissions.sql';
END IF;
IF to_regprocedure('v3.user_role_code(int)') IS NULL THEN
RAISE EXCEPTION 'Зависимость отсутствует: v3.user_role_code(INT). Накатите sql/v3/permissions.sql';
END IF;
END
$precheck$;
DROP FUNCTION IF EXISTS v3.v_form_view(INT, VARCHAR, TEXT[], VARCHAR);
DROP FUNCTION IF EXISTS v3.v_form_view(INT, VARCHAR, TEXT[], VARCHAR, INT);
CREATE FUNCTION v3.v_form_view(
p_form_id INT,
p_sheet VARCHAR,
p_sections TEXT[] DEFAULT NULL,
p_direction VARCHAR DEFAULT NULL,
p_user_id INT DEFAULT NULL
)
RETURNS TABLE (
row_type VARCHAR,
depth INT,
sort_order BIGINT,
data JSONB
)
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;
-- ═══ META-строка (маска редактируемых колонок) ════════════════════════════
-- Отдаётся первой (sort_order=0), только если передан p_user_id. НЕ гейт на
-- просмотр — сами строки листа возвращаются ниже независимо от прав.
IF p_user_id IS NOT NULL THEN
RETURN QUERY
SELECT
'META'::VARCHAR,
-1,
0::BIGINT,
jsonb_build_object(
'user_id', p_user_id,
'role', v3.user_role_code(p_user_id),
'sheet', p_sheet,
'editable', COALESCE(
(SELECT jsonb_agg(
jsonb_build_object('column', ec.column_key,
'closes_at', ec.closes_at)
ORDER BY ec.column_key)
FROM v3.editable_columns_for(p_form_id, p_user_id, p_sheet) ec),
'[]'::JSONB)
);
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$
LANGUAGE plpgsql STABLE;
-- ════════════════════════════════════════════════════════════════════════════
-- ПАТЧ: v3.v_form3_report_jsonb — добавление p_user_id + META-строки (editable)
-- ════════════════════════════════════════════════════════════════════════════
-- Идемпотентен. Пересоздаёт функцию (DROP 2-арг и 3-арг сигнатур + CREATE).
-- Зависимости (должны быть на целевой БД): v3.editable_columns_for3(INT,INT),
-- v3.user_role_code(INT), v3.rf_project_report, v3.v_form3_report_sections.
--
-- Применение:
-- psql -U dfip -d dfip -f patch_v_form3_report_jsonb_meta.sql
-- docker exec -i <container> psql -U dfip -d dfip < patch_v_form3_report_jsonb_meta.sql
-- ════════════════════════════════════════════════════════════════════════════
DO $precheck$
BEGIN
IF to_regprocedure('v3.editable_columns_for3(int,int)') IS NULL THEN
RAISE EXCEPTION 'Зависимость отсутствует: v3.editable_columns_for3(INT,INT). Накатите sql/v3/permissions_form3.sql';
END IF;
IF to_regprocedure('v3.user_role_code(int)') IS NULL THEN
RAISE EXCEPTION 'Зависимость отсутствует: v3.user_role_code(INT). Накатите sql/v3/permissions.sql';
END IF;
END
$precheck$;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.v_form3_report_jsonb — JSONB-обёртка над v_form3_report_sections
-- ════════════════════════════════════════════════════════════════════════════
-- Один трек FORM_3 (LIMIT либо CURRENT_EXPENSES) как (row_type, depth,
-- sort_order, data jsonb) — общий формат UI с FORM_1/2/4.
--
-- p_user_id — если задан, ПЕРВОЙ строкой (row_type='META', depth=-1,
-- sort_order=0) идёт маска редактируемых колонок текущей фазы для этого юзера:
-- data = { user_id, role, report_id, report_type, editable:[{column,closes_at}] }.
-- Маска — v3.editable_columns_for3(report, user) (якорь rf_project_report,
-- лист/трек определяется отчётом внутри). Это НЕ гейт на просмотр — строки
-- отчёта отдаются всегда; при отсутствии прав editable=[] → фронт read-only.
-- NULL (по умолчанию) → META-строки нет (обратная совместимость).
-- ════════════════════════════════════════════════════════════════════════════
DROP FUNCTION IF EXISTS v3.v_form3_report_jsonb(INT, TEXT[]);
DROP FUNCTION IF EXISTS v3.v_form3_report_jsonb(INT, TEXT[], INT);
CREATE FUNCTION v3.v_form3_report_jsonb(
p_report_id INT,
p_sections TEXT[] DEFAULT NULL,
p_user_id INT DEFAULT NULL
)
RETURNS TABLE (
row_type VARCHAR,
depth INT,
sort_order BIGINT,
data JSONB
)
AS $function$
SELECT u.row_type, u.depth, u.sort_order, u.data
FROM (
-- ═══ META-строка (маска редактируемых колонок) ══════════════════════════
-- Только при переданном p_user_id. НЕ гейт на просмотр.
SELECT
'META'::VARCHAR AS row_type,
-1 AS depth,
0::BIGINT AS sort_order,
jsonb_build_object(
'user_id', p_user_id,
'role', v3.user_role_code(p_user_id),
'report_id', p_report_id,
'report_type', (SELECT report_type FROM v3.rf_project_report WHERE id = p_report_id),
'editable', COALESCE(
(SELECT jsonb_agg(
jsonb_build_object('column', ec.column_key,
'closes_at', ec.closes_at)
ORDER BY ec.column_key)
FROM v3.editable_columns_for3(p_report_id, p_user_id) ec),
'[]'::JSONB)
) AS data
WHERE p_user_id IS NOT NULL
UNION ALL
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(
'base_plan', s.col_q1_base_plan,
'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(
'base_plan', s.col_q2_base_plan,
'corrected_plan', s.col_q2_corrected_plan,
'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(
'base_plan', s.col_q3_base_plan,
'corrected_plan', s.col_q3_corrected_plan,
'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(
'base_plan', s.col_q4_base_plan,
'corrected_plan', s.col_q4_corrected_plan,
'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(
'base_plan', s.col_year_base_plan, -- Год Базового плана (Σ q1q4)
'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
) u
ORDER BY u.sort_order;
$function$
LANGUAGE sql STABLE;