-- удаление проектов
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,   -- Год Базового плана (Σ q1–q4)
          '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;

