-- DROP FUNCTION v3._apply_form3_cell(int4, text, jsonb);

CREATE OR REPLACE FUNCTION v3._apply_form3_cell(p_line_id integer, p_column text, p_value jsonb)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_parts       TEXT[];
  v_scope       TEXT;
  v_field       TEXT;
  v_q           SMALLINT;
  v_target_col  TEXT;
  v_target_type TEXT;
  v_sql         TEXT;
BEGIN
  v_parts := string_to_array(p_column, '.');
  IF array_length(v_parts, 1) < 2 THEN
    RAISE EXCEPTION 'bad_column_format: %, expected scope.field', p_column;
  END IF;
  v_scope := v_parts[1];
  v_field := v_parts[2];

  IF v_scope = 'totals' OR v_field IN ('total_corr','quarter_actual','economy','carryover') THEN
    RAISE EXCEPTION 'computed_field: %', p_column;
  END IF;

  IF v_scope NOT IN ('q1','q2','q3','q4') THEN
    RAISE EXCEPTION 'unknown_scope: % (FORM_3 supports only q1..q4)', v_scope;
  END IF;

  v_q := substring(v_scope FROM 2)::SMALLINT;

  -- Маппинг JSONB-ключ → колонка rf_project_report_quarter
  CASE v_field
    WHEN 'adj_by_items' THEN v_target_col := 'adj_by_items'; v_target_type := 'NUMERIC';
    WHEN 'adj_increase' THEN v_target_col := 'adj_increase'; v_target_type := 'NUMERIC';
    WHEN 'm1'           THEN v_target_col := 'actual_m1';    v_target_type := 'NUMERIC';
    WHEN 'm2'           THEN v_target_col := 'actual_m2';    v_target_type := 'NUMERIC';
    WHEN 'm3'           THEN v_target_col := 'actual_m3';    v_target_type := 'NUMERIC';
    WHEN 'spod' THEN
      IF v_q <> 4 THEN RAISE EXCEPTION 'spod is q4-only: %', p_column; END IF;
      v_target_col := 'actual_spod'; v_target_type := 'NUMERIC';
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
  END CASE;

  v_sql := format(
    'INSERT INTO v3.rf_project_report_quarter (rf_project_report_line_id, quarter, %1$I) '
    || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) '
    || 'ON CONFLICT (rf_project_report_line_id, quarter) DO UPDATE SET %1$I = EXCLUDED.%1$I',
    v_target_col, v_target_type
  );
  EXECUTE v_sql USING p_line_id, v_q, p_value;
END;
$function$
;

-- DROP FUNCTION v3._apply_form_cell(int4, text, jsonb, varchar, int4);

CREATE OR REPLACE FUNCTION v3._apply_form_cell(p_line_id integer, p_column text, p_value jsonb, p_sheet character varying DEFAULT NULL::character varying, p_form_id integer DEFAULT NULL::integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_parts        TEXT[];
  v_scope        TEXT;
  v_field        TEXT;
  v_q            SMALLINT;
  v_actor        TEXT;
  v_target_table TEXT;
  v_target_col   TEXT;
  v_target_type  TEXT;
  v_key_kind     TEXT;
  v_str          TEXT;
  v_sql          TEXT;
  v_sat_table    TEXT;
  v_cnt          INT;
BEGIN
  v_parts := string_to_array(p_column, '.');

  -- ═══ OTCH9F (fixed_asset_report) ══════════════════════════════════════
  -- p_line_id = fixed_asset_report.id.
  -- Editable: b_604.{acquired,disposed}_*, b_60415.{acquired,transferred}_*,
  --           totals.go_balance_only;
  --           b_604.opening_*, b_60415.opening_* — ТОЛЬКО для month=1 (для
  --           месяцев 2..12 это computed накопительно из месяца 1).
  -- RO: equipment_name (нормативная номенклатура), month/expense_item_id/
  --     item_id/section_code (ключ строки), totals.{acquired_total,
  --     disposed_total, balance_qty, balance_amt} (computed).
  IF p_sheet = 'OTCH9F' THEN
    DECLARE v_far_month SMALLINT;
    BEGIN
      SELECT month INTO v_far_month FROM v3.fixed_asset_report WHERE id = p_line_id;
      IF v_far_month IS NULL THEN
        RAISE EXCEPTION 'fixed_asset_report.id=% не существует', p_line_id;
      END IF;

      -- Одноуровневые ключи — только нормативные/ключевые → RO
      IF array_length(v_parts,1) = 1 THEN
        IF v_parts[1] = 'equipment_name' THEN
          RAISE EXCEPTION 'normative_field: equipment_name (нормативная номенклатура, правится отдельным API)';
        ELSIF v_parts[1] IN ('month','expense_item_id','item_id','section_code','id') THEN
          RAISE EXCEPTION 'key_field: % (часть ключа строки, RO)', v_parts[1];
        ELSE
          RAISE EXCEPTION 'unknown_column: %', p_column;
        END IF;
      END IF;

      IF array_length(v_parts,1) <> 2 THEN
        RAISE EXCEPTION 'bad_column_format: %', p_column;
      END IF;

      -- Двухуровневые: totals.x / b_604.x / b_60415.x
      IF v_parts[1] = 'totals' THEN
        IF v_parts[2] = 'go_balance_only' THEN
          v_target_col := 'go_balance_only_amt'; v_target_type := 'NUMERIC';
        ELSIF v_parts[2] IN ('acquired_total','disposed_total','balance_qty','balance_amt') THEN
          RAISE EXCEPTION 'computed_field: totals.%', v_parts[2];
        ELSE
          RAISE EXCEPTION 'unknown_column: %', p_column;
        END IF;
      ELSIF v_parts[1] IN ('b_604','b_60415') THEN
        -- opening_* для месяцев 2..12 — computed
        IF v_parts[2] LIKE 'opening_%' AND v_far_month <> 1 THEN
          RAISE EXCEPTION 'computed_field: % (opening для month>1 вычисляется из base + накопит. дельты)', p_column;
        END IF;
        -- Маппинг b_604/60415 → колонки таблицы
        IF v_parts[1] = 'b_604' THEN
          v_target_col := CASE v_parts[2]
            WHEN 'opening_qty'  THEN 'opening_qty_604'
            WHEN 'opening_amt'  THEN 'opening_amt_604'
            WHEN 'acquired_qty' THEN 'acquired_qty_604'
            WHEN 'acquired_amt' THEN 'acquired_amt_604'
            WHEN 'disposed_qty' THEN 'disposed_qty_604'
            WHEN 'disposed_amt' THEN 'disposed_amt_604'
          END;
          IF v_target_col IS NULL THEN
            RAISE EXCEPTION 'unknown_column: %', p_column;
          END IF;
          v_target_type := CASE WHEN v_parts[2] LIKE '%_qty' THEN 'INTEGER' ELSE 'NUMERIC' END;
        ELSE  -- b_60415
          v_target_col := CASE v_parts[2]
            WHEN 'opening_qty'     THEN 'opening_qty_60415'
            WHEN 'opening_amt'     THEN 'opening_amt_60415'
            WHEN 'acquired_qty'    THEN 'acquired_qty_60415'
            WHEN 'acquired_amt'    THEN 'acquired_amt_60415'
            WHEN 'transferred_qty' THEN 'transferred_qty_60415'
            WHEN 'transferred_amt' THEN 'transferred_amt_60415'
          END;
          IF v_target_col IS NULL THEN
            RAISE EXCEPTION 'unknown_column: %', p_column;
          END IF;
          v_target_type := CASE WHEN v_parts[2] LIKE '%_qty' THEN 'INTEGER' ELSE 'NUMERIC' END;
        END IF;
      ELSE
        RAISE EXCEPTION 'unknown_column: %', p_column;
      END IF;

      EXECUTE format(
        'UPDATE v3.fixed_asset_report SET %I = ($2 #>> ''{}'')::%s WHERE id = $1',
        v_target_col, v_target_type
      ) USING p_line_id, p_value;
      RETURN;
    END;
  END IF;

  -- ═══ AHR_LIMIT (limit_template + form_limit) ═════════════════════════
  -- p_line_id здесь = limit_template.id LEAF (глобальный каталог нормативов).
  -- Editable: qty_q1..q4 (INTEGER), comment (TEXT) — пишутся в v3.form_limit
  --   (UPSERT по (budget_form_id, template_id)).
  -- amount_q1..q4 — computed по формуле в read (qty × limit × period_factor).
  -- name/unit/limit_*/section_no/expense_item_code — нормативный справочник
  -- (правится отдельным админ-API, не через write UI).
  IF p_sheet = 'AHR_LIMIT' THEN
    IF p_form_id IS NULL THEN
      RAISE EXCEPTION 'internal: p_form_id не передан для AHR_LIMIT';
    END IF;

    -- Sanity: строка должна быть LEAF
    IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt
                   WHERE lt.id = p_line_id AND lt.row_type = 'LEAF') THEN
      RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF (SECTION/GROUP не редактируется)', p_line_id;
    END IF;

    IF array_length(v_parts,1) <> 1 THEN
      RAISE EXCEPTION 'bad_column_format: %, expected single key', p_column;
    END IF;

    CASE v_parts[1]
      WHEN 'qty_q1','qty_q2','qty_q3','qty_q4' THEN
        v_target_col := v_parts[1]; v_target_type := 'INTEGER';
      WHEN 'comment' THEN
        v_target_col := 'comment';  v_target_type := 'TEXT';
      WHEN 'amount_q1','amount_q2','amount_q3','amount_q4' THEN
        RAISE EXCEPTION 'computed_field: % (вычисляется из qty × limit × period_factor)', p_column;
      WHEN 'name','unit','section_no','expense_item_code',
           'limit_with_vat','limit_without_vat' THEN
        RAISE EXCEPTION 'normative_field: % (нормативный справочник, правится админом отдельно)', p_column;
      ELSE
        RAISE EXCEPTION 'unknown_column: %', p_column;
    END CASE;

    EXECUTE format(
      'INSERT INTO v3.form_limit (budget_form_id, template_id, %1$I) '
      || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) '
      || 'ON CONFLICT (budget_form_id, template_id) DO UPDATE SET %1$I = EXCLUDED.%1$I',
      v_target_col, v_target_type
    ) USING p_form_id, p_line_id, p_value;
    RETURN;
  END IF;

  -- ═══ Сателлиты (AHR_RENT / AHR_UTILITY / AHR_SECURITY) ═══════════════
  -- Здесь p_line_id трактуется как id строки сателлита (rent/utility/
  -- security_detail.id) — НЕ budget_line.id. UPDATE по этому id; INSERT/
  -- DELETE — через add_budget_line / del_budget_line с тем же sheet.
  IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
    v_sat_table := CASE p_sheet
      WHEN 'AHR_RENT'     THEN 'rent_detail'
      WHEN 'AHR_UTILITY'  THEN 'utility_detail'
      WHEN 'AHR_SECURITY' THEN 'security_detail'
    END;

    -- Парсим (ключи бывают одно- и двухуровневые)
    IF array_length(v_parts,1) = 1 THEN
      CASE v_parts[1]
        WHEN 'contract_number'   THEN v_target_col := 'contract_number';   v_target_type := 'TEXT';
        WHEN 'contract_end_date' THEN v_target_col := 'contract_end_date'; v_target_type := 'DATE';
        WHEN 'comment'           THEN v_target_col := 'comment';           v_target_type := 'TEXT';
        WHEN 'address','object_type','rented_area','object_area' THEN
          RAISE EXCEPTION 'computed_field: % (атрибут v3.vsp, правится отдельно)', p_column;
        ELSE RAISE EXCEPTION 'unknown_column: %', p_column;
      END CASE;
    ELSIF array_length(v_parts,1) = 2 AND v_parts[1] = 'plan' THEN
      IF v_parts[2] = 'year' THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF;
      IF v_parts[2] NOT IN ('q1','q2','q3','q4') THEN RAISE EXCEPTION 'unknown_column: %', p_column; END IF;
      v_target_col := 'plan_' || v_parts[2];
      v_target_type := 'NUMERIC';
    ELSIF array_length(v_parts,1) = 2 AND v_parts[1] LIKE 'fact_q%' THEN
      IF v_parts[2] = 'total' THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF;
      -- белый список месяцев по соответствующему кварталу
      IF (v_parts[1]='fact_q1' AND v_parts[2] NOT IN ('jan','feb','mar')) OR
         (v_parts[1]='fact_q2' AND v_parts[2] NOT IN ('apr','may','jun')) OR
         (v_parts[1]='fact_q3' AND v_parts[2] NOT IN ('jul','aug','sep')) OR
         (v_parts[1]='fact_q4' AND v_parts[2] NOT IN ('oct','nov','dec')) THEN
        RAISE EXCEPTION 'unknown_column: % (месяц вне квартала)', p_column;
      END IF;
      v_target_col := 'actual_' || v_parts[2];
      v_target_type := 'NUMERIC';
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
    END IF;

    EXECUTE format(
      'UPDATE v3.%I SET %I = ($2 #>> ''{}'')::%s WHERE id = $1',
      v_sat_table, v_target_col, v_target_type
    ) USING p_line_id, p_value;

    GET DIAGNOSTICS v_cnt = ROW_COUNT;
    IF v_cnt = 0 THEN
      RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_line_id;
    END IF;
    RETURN;
  END IF;

  -- ═══ Основная сетка (FORM_1/2/4 AHR/CAP/OPER) ════════════════════════
  IF array_length(v_parts, 1) < 2 THEN
    RAISE EXCEPTION 'bad_column_format: %, expected scope.field', p_column;
  END IF;
  v_scope := v_parts[1];
  v_field := v_parts[2];

  -- ── header: editable budget_line columns ──────────────────────────────
  IF v_scope = 'header' THEN
    IF v_field = 'name' THEN
      UPDATE v3.budget_line SET name = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id;
    ELSIF v_field = 'internal_order' THEN
      UPDATE v3.budget_line SET internal_order = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id;
    ELSIF v_field = 'vsp_id' THEN
      UPDATE v3.budget_line SET vsp_id = (p_value #>> '{}')::INT, updated_at = now() WHERE id = p_line_id;
    ELSIF v_field IN ('year','section','item_id','num_group','vsp_address') THEN
      RAISE EXCEPTION 'computed_field: %', p_column;
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
    END IF;
    RETURN;
  END IF;

  -- ── computed scopes (RO) ──────────────────────────────────────────────
  IF v_scope IN ('approved','totals') THEN
    RAISE EXCEPTION 'computed_field: %', p_column;
  END IF;

  -- ── booking.y{2026|2027}.qN → v3.ckk (см. read FORM_2/4 jsonb) ────────
  -- В read-обёртках booking синтезируется из ckk.expenses_qN (= y2026,
  -- текущий год) и ckk.expenses_next_year_qN (= y2027, следующий).
  -- Имена y2026/y2027 захардкожены в read; повторяем то же в write.
  IF v_scope = 'booking' THEN
    IF array_length(v_parts, 1) <> 3 THEN
      RAISE EXCEPTION 'bad_column_format: %, expected booking.y{2026|2027}.qN', p_column;
    END IF;
    IF v_parts[2] NOT IN ('y2026','y2027') THEN
      RAISE EXCEPTION 'bad_booking_year: % (only y2026/y2027)', v_parts[2];
    END IF;
    IF v_parts[3] NOT IN ('q1','q2','q3','q4') THEN
      RAISE EXCEPTION 'bad_booking_quarter: %', v_parts[3];
    END IF;
    v_target_col := CASE v_parts[2]
      WHEN 'y2026' THEN 'expenses_'           || v_parts[3]
      WHEN 'y2027' THEN 'expenses_next_year_' || v_parts[3]
    END;
    v_sql := format(
      'INSERT INTO v3.ckk (line_id, %1$I) VALUES ($1, ($2 #>> ''{}'')::NUMERIC) '
      || 'ON CONFLICT (line_id) DO UPDATE SET %1$I = EXCLUDED.%1$I',
      v_target_col
    );
    EXECUTE v_sql USING p_line_id, p_value;
    UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id;
    RETURN;
  END IF;

  -- ── Маппинг (scope, field) → (table, col, type, key_kind, actor) ─────
  --   key_kind: 'line'        — PK/UQ = (line_id)
  --             'line_quarter'— PK    = (line_id, quarter), quarter из v_q (берётся из scope qN)
  --             'line_actor'  — UQ    = (line_id, actor), actor из v_actor
  --
  -- VALUES — это и есть white-list редактируемых полей. Поля, отсутствующие
  -- здесь (computed: corrected_plan, new_plan, actual_quarter, rem_*, economy,
  -- total, booking; FORM_2 q1.adj_rf — нет такого поля у FORM_2; и т.п.) →
  -- падают с unknown_column ниже.

  -- Quarter scope qN → ставим v_q и нормализуем scope в 'q' для маппинга
  IF v_scope IN ('q1','q2','q3','q4') THEN
    v_q := substring(v_scope FROM 2)::SMALLINT;
  ELSE
    v_q := NULL;
  END IF;

  -- Actor для sequestration scopes
  v_actor := CASE v_scope
    WHEN 'sequestration' THEN 'DFIP'
    WHEN 'seq_dfip'      THEN 'DFIP'
    WHEN 'seq_ssp'       THEN 'SSP_GO'
    ELSE NULL END;

  SELECT m.target_table, m.target_col, m.target_type, m.key_kind
    INTO v_target_table, v_target_col, v_target_type, v_key_kind
  FROM (VALUES
    -- ─── plan ──────────────────────────────────────────────────────────
    ('plan','q1',      'plan','plan_q1','NUMERIC','line'),
    ('plan','q2',      'plan','plan_q2','NUMERIC','line'),
    ('plan','q3',      'plan','plan_q3','NUMERIC','line'),
    ('plan','q4',      'plan','plan_q4','NUMERIC','line'),
    ('plan','comment', 'plan','comment','TEXT',   'line'),

    -- ─── reserve ───────────────────────────────────────────────────────
    ('reserve','q1',           'reserve','amount_q1','NUMERIC','line'),
    ('reserve','q2',           'reserve','amount_q2','NUMERIC','line'),
    ('reserve','q3',           'reserve','amount_q3','NUMERIC','line'),
    ('reserve','q4',           'reserve','amount_q4','NUMERIC','line'),
    ('reserve','justification','reserve','justification','TEXT','line'),

    -- ─── allocation (FORM_1/4) ─────────────────────────────────────────
    ('allocation','order',    'allocation','internal_order','TEXT','line'),
    ('allocation','property', 'allocation','property_object','TEXT','line'),

    -- ─── contract_summary (FORM_1/4) ───────────────────────────────────
    ('contract_summary','total',        'contract_summary','total_amount',       'NUMERIC','line'),
    ('contract_summary','counterparty', 'contract_summary','counterparty',       'TEXT',   'line'),
    ('contract_summary','deadline',     'contract_summary','deadline',           'TEXT',   'line'),
    ('contract_summary','comment',      'contract_summary','comment',            'TEXT',   'line'),
    ('contract_summary','future_y1',    'contract_summary','future_payments_y1', 'NUMERIC','line'),
    ('contract_summary','future_y2',    'contract_summary','future_payments_y2', 'NUMERIC','line'),
    ('contract_summary','other_ssp',    'contract_summary','other_ssp_amount',   'NUMERIC','line'),

    -- ─── collegial_approval (FORM_1/4) ─────────────────────────────────
    ('collegial','approved', 'collegial_approval','approved_amount',   'NUMERIC','line'),
    ('collegial','protocol', 'collegial_approval','protocol_reference','TEXT',   'line'),
    ('collegial','note',     'collegial_approval','note',              'TEXT',   'line'),

    -- ─── ckk (FORM_1/4) ────────────────────────────────────────────────
    ('ckk','ceiling',     'ckk','ceiling_amount',    'NUMERIC','line'),
    ('ckk','q1',          'ckk','expenses_q1',           'NUMERIC','line'),
    ('ckk','q2',          'ckk','expenses_q2',           'NUMERIC','line'),
    ('ckk','q3',          'ckk','expenses_q3',           'NUMERIC','line'),
    ('ckk','q4',          'ckk','expenses_q4',           'NUMERIC','line'),
    ('ckk','next_q1',     'ckk','expenses_next_year_q1', 'NUMERIC','line'),
    ('ckk','next_q2',     'ckk','expenses_next_year_q2', 'NUMERIC','line'),
    ('ckk','next_q3',     'ckk','expenses_next_year_q3', 'NUMERIC','line'),
    ('ckk','next_q4',     'ckk','expenses_next_year_q4', 'NUMERIC','line'),
    ('ckk','rf_schedule', 'ckk','rf_schedule',       'TEXT',   'line'),
    ('ckk','deadline',    'ckk','delivery_deadline', 'TEXT',   'line'),
    ('ckk','proc_plan',   'ckk','procurement_plan',  'TEXT',   'line'),
    ('ckk','proc_method', 'ckk','procurement_method','TEXT',   'line'),
    ('ckk','comment',     'ckk','comment',           'TEXT',   'line'),

    -- ─── contract_detail (FORM_1) и его alias contract (FORM_2/4) ─────
    ('contract_detail','counterparty',   'contract_detail','counterparty',  'TEXT',   'line'),
    ('contract_detail','reference',      'contract_detail','reference',     'TEXT',   'line'),
    ('contract_detail','addenda',        'contract_detail','addenda',       'TEXT',   'line'),
    ('contract_detail','subject',        'contract_detail','subject',       'TEXT',   'line'),
    ('contract_detail','currency',       'contract_detail','currency',      'TEXT',   'line'),
    ('contract_detail','ceiling',        'contract_detail','ceiling_amount','NUMERIC','line'),
    ('contract_detail','q1',             'contract_detail','expenses_q1',   'NUMERIC','line'),
    ('contract_detail','q2',             'contract_detail','expenses_q2',   'NUMERIC','line'),
    ('contract_detail','q3',             'contract_detail','expenses_q3',   'NUMERIC','line'),
    ('contract_detail','q4',             'contract_detail','expenses_q4',   'NUMERIC','line'),
    ('contract_detail','rf_schedule',    'contract_detail','rf_schedule',   'TEXT',   'line'),
    ('contract_detail','vat_rate',       'contract_detail','vat_rate',      'TEXT',   'line'),
    ('contract_detail','exchange_rate',  'contract_detail','exchange_rate', 'NUMERIC','line'),
    ('contract_detail','amount_foreign', 'contract_detail','amount_foreign','NUMERIC','line'),
    ('contract_detail','deadline',       'contract_detail','deadline',      'TEXT',   'line'),
    ('contract_detail','payment_scheme', 'contract_detail','payment_scheme','TEXT',   'line'),
    ('contract_detail','act',            'contract_detail','act',           'TEXT',   'line'),
    ('contract_detail','comment',        'contract_detail','comment',       'TEXT',   'line'),
    ('contract_detail','contract_date',  'contract_detail','contract_date', 'DATE',   'line'),

    ('contract','counterparty', 'contract_detail','counterparty',  'TEXT',   'line'),
    ('contract','reference',    'contract_detail','reference',     'TEXT',   'line'),
    ('contract','date',         'contract_detail','contract_date', 'DATE',   'line'),
    ('contract','subject',      'contract_detail','subject',       'TEXT',   'line'),
    ('contract','currency',     'contract_detail','currency',      'TEXT',   'line'),
    ('contract','ceiling',      'contract_detail','ceiling_amount','NUMERIC','line'),
    ('contract','vat_rate',     'contract_detail','vat_rate',      'TEXT',   'line'),
    ('contract','deadline',     'contract_detail','deadline',      'TEXT',   'line'),
    ('contract','scheme',       'contract_detail','payment_scheme','TEXT',   'line'),
    ('contract','act',          'contract_detail','act',           'TEXT',   'line'),
    ('contract','comment',      'contract_detail','comment',       'TEXT',   'line'),

    -- ─── sequestration (line_id, actor) — три алиаса scope ────────────
    ('sequestration','q1',           'sequestration','adj_q1',       'NUMERIC','line_actor'),
    ('sequestration','q2',           'sequestration','adj_q2',       'NUMERIC','line_actor'),
    ('sequestration','q3',           'sequestration','adj_q3',       'NUMERIC','line_actor'),
    ('sequestration','q4',           'sequestration','adj_q4',       'NUMERIC','line_actor'),
    ('sequestration','justification','sequestration','justification','TEXT',   'line_actor'),

    ('seq_dfip','q1',           'sequestration','adj_q1',       'NUMERIC','line_actor'),
    ('seq_dfip','q2',           'sequestration','adj_q2',       'NUMERIC','line_actor'),
    ('seq_dfip','q3',           'sequestration','adj_q3',       'NUMERIC','line_actor'),
    ('seq_dfip','q4',           'sequestration','adj_q4',       'NUMERIC','line_actor'),
    ('seq_dfip','justification','sequestration','justification','TEXT',   'line_actor'),

    ('seq_ssp','q1',           'sequestration','adj_q1',       'NUMERIC','line_actor'),
    ('seq_ssp','q2',           'sequestration','adj_q2',       'NUMERIC','line_actor'),
    ('seq_ssp','q3',           'sequestration','adj_q3',       'NUMERIC','line_actor'),
    ('seq_ssp','q4',           'sequestration','adj_q4',       'NUMERIC','line_actor'),
    ('seq_ssp','justification','sequestration','justification','TEXT',   'line_actor'),

    -- ─── budget_line_quarter — все 4 квартала через нормализованный scope='q' ─
    -- (v_scope qN, v_q заполнен; маппинг ниже использует scope='q')
    ('q','adj_current',                 'budget_line_quarter','adj_current',                'NUMERIC','line_quarter'),
    ('q','adj_ssp',                     'budget_line_quarter','adj_ssp',                    'NUMERIC','line_quarter'),
    ('q','adj_rf',                      'budget_line_quarter','adj_rf',                     'NUMERIC','line_quarter'),
    ('q','adj_reserve',                 'budget_line_quarter','adj_reserve',                'NUMERIC','line_quarter'),
    ('q','adj_comment',                 'budget_line_quarter','adj_comment',                'TEXT',   'line_quarter'),
    ('q','target_change',               'budget_line_quarter','target_change',              'NUMERIC','line_quarter'),
    ('q','base_correction',             'budget_line_quarter','base_plan_correction',       'NUMERIC','line_quarter'),
    ('q','base_correction_comment',     'budget_line_quarter','base_plan_correction_comment','TEXT',  'line_quarter'),
    ('q','pay_date',                    'budget_line_quarter','payment_date',               'DATE',   'line_quarter'),
    ('q','pay_amount',                  'budget_line_quarter','payment_amount',             'NUMERIC','line_quarter'),
    ('q','pay_ho',                      'budget_line_quarter','payment_amount_ho',          'NUMERIC','line_quarter'),
    ('q','pay_rf',                      'budget_line_quarter','payment_amount_rf',          'NUMERIC','line_quarter'),
    ('q','pay_comment',                 'budget_line_quarter','payment_comment',            'TEXT',   'line_quarter'),
    ('q','pay_act',                     'budget_line_quarter','payment_act',                'TEXT',   'line_quarter'),
    ('q','actual_m1',                   'budget_line_quarter','actual_m1',                  'NUMERIC','line_quarter'),
    ('q','actual_m2',                   'budget_line_quarter','actual_m2',                  'NUMERIC','line_quarter'),
    ('q','actual_m3',                   'budget_line_quarter','actual_m3',                  'NUMERIC','line_quarter'),
    ('q','actual_spod',                 'budget_line_quarter','actual_spod',                'NUMERIC','line_quarter'),
    ('q','transfer_q2',                 'budget_line_quarter','transfer_to_q2',             'NUMERIC','line_quarter'),
    ('q','transfer_q3',                 'budget_line_quarter','transfer_to_q3',             'NUMERIC','line_quarter'),
    ('q','transfer_q4',                 'budget_line_quarter','transfer_to_q4',             'NUMERIC','line_quarter'),
    ('q','transfer_econ',               'budget_line_quarter','transfer_to_economy',        'NUMERIC','line_quarter'),
    -- разбивка переноса в следующий квартал (transfer_q{N+1}_*) — единые поля,
    -- индекс N+1 в имени ключа для UI смыслово важен, в БД одно и то же поле:
    ('q','transfer_q2_delay_acts',        'budget_line_quarter','transfer_delay_acts',        'NUMERIC','line_quarter'),
    ('q','transfer_q2_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'),
    ('q','transfer_q2_economy_rf',        'budget_line_quarter','transfer_economy_rf',        'NUMERIC','line_quarter'),
    ('q','transfer_q3_delay_acts',        'budget_line_quarter','transfer_delay_acts',        'NUMERIC','line_quarter'),
    ('q','transfer_q3_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'),
    ('q','transfer_q3_economy_rf',        'budget_line_quarter','transfer_economy_rf',        'NUMERIC','line_quarter'),
    ('q','transfer_q4_delay_acts',        'budget_line_quarter','transfer_delay_acts',        'NUMERIC','line_quarter'),
    ('q','transfer_q4_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'),
    ('q','transfer_q4_economy_rf',        'budget_line_quarter','transfer_economy_rf',        'NUMERIC','line_quarter'),
    ('q','transfer_next_comment',         'budget_line_quarter','transfer_next_comment',      'TEXT',   'line_quarter'),
    ('q','transfer_far_comment',          'budget_line_quarter','transfer_far_comment',       'TEXT',   'line_quarter'),
    -- plan_revision_* — две группы алиасов: FORM_1 (rev_*) и FORM_2/4 (revision_*).
    ('q','rev_eco',          'budget_line_quarter','plan_revision_eco_change','NUMERIC','line_quarter'),
    ('q','rev_item',         'budget_line_quarter','plan_revision_item_adj',  'NUMERIC','line_quarter'),
    ('q','rev_inc',          'budget_line_quarter','plan_revision_increase',  'NUMERIC','line_quarter'),
    ('q','rev_seq',          'budget_line_quarter','plan_revision_sequester', 'NUMERIC','line_quarter'),
    ('q','rev_comment',      'budget_line_quarter','plan_revision_comment',   'TEXT',   'line_quarter'),
    ('q','revision_inc',     'budget_line_quarter','plan_revision_increase',  'NUMERIC','line_quarter'),
    ('q','revision_seq',     'budget_line_quarter','plan_revision_sequester', 'NUMERIC','line_quarter'),
    ('q','revision_comment', 'budget_line_quarter','plan_revision_comment',   'TEXT',   'line_quarter'),
    ('q','booking_amount',   'budget_line_quarter','booking_amount',          'NUMERIC','line_quarter')
  ) AS m(scope, field, target_table, target_col, target_type, key_kind)
  WHERE m.scope = (CASE WHEN v_q IS NOT NULL THEN 'q' ELSE v_scope END)
    AND m.field = v_field;

  IF v_target_table IS NULL THEN
    -- Распознаваемые computed-ключи отдельно — для понятной ошибки:
    IF v_field IN ('corrected_plan','new_plan','actual_quarter','booking',
                   'rem_booking','rem_actual','residual_after_booking',
                   'residual_after_actual','economy','total','year') THEN
      RAISE EXCEPTION 'computed_field: %', p_column;
    END IF;
    RAISE EXCEPTION 'unknown_column: %', p_column;
  END IF;

  -- Извлекаем скаляр из JSONB — '#>> {}' возвращает NULL для jsonb null
  -- Cast прицельный (NUMERIC/TEXT/DATE/INT) делается в dynamic SQL ниже.

  -- ── Dynamic UPSERT ────────────────────────────────────────────────────
  IF v_key_kind = 'line' THEN
    v_sql := format(
      'INSERT INTO v3.%1$I (line_id, %2$I) VALUES ($1, ($2 #>> ''{}'')::%3$s) '
      || 'ON CONFLICT (line_id) DO UPDATE SET %2$I = EXCLUDED.%2$I',
      v_target_table, v_target_col, v_target_type
    );
    EXECUTE v_sql USING p_line_id, p_value;

  ELSIF v_key_kind = 'line_quarter' THEN
    IF v_q IS NULL THEN RAISE EXCEPTION 'internal: quarter not set for %', p_column; END IF;
    v_sql := format(
      'INSERT INTO v3.%1$I (line_id, quarter, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) '
      || 'ON CONFLICT (line_id, quarter) DO UPDATE SET %2$I = EXCLUDED.%2$I',
      v_target_table, v_target_col, v_target_type
    );
    EXECUTE v_sql USING p_line_id, v_q, p_value;

  ELSIF v_key_kind = 'line_actor' THEN
    IF v_actor IS NULL THEN RAISE EXCEPTION 'internal: actor not set for %', p_column; END IF;
    -- Для quarter-полей sequestration: scope.field=qN→adj_qN, justification без quarter
    v_sql := format(
      'INSERT INTO v3.%1$I (line_id, actor, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) '
      || 'ON CONFLICT (line_id, actor) DO UPDATE SET %2$I = EXCLUDED.%2$I',
      v_target_table, v_target_col, v_target_type
    );
    EXECUTE v_sql USING p_line_id, v_actor, p_value;

  ELSE
    RAISE EXCEPTION 'internal: unknown key_kind %', v_key_kind;
  END IF;

  -- Помечаем budget_line как изменённую (для updated_at iteration)
  UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id;
END;
$function$
;

-- DROP FUNCTION v3._diff_val(anyelement);

CREATE OR REPLACE FUNCTION v3._diff_val(p_val anyelement)
 RETURNS jsonb
 LANGUAGE sql
 IMMUTABLE
AS $function$
    SELECT CASE WHEN p_val IS NULL THEN 'null'::jsonb
                ELSE to_jsonb(p_val::TEXT) END;
$function$
;

-- DROP FUNCTION v3._expense_item_path_set(_int4);

CREATE OR REPLACE FUNCTION v3._expense_item_path_set(p_eids integer[])
 RETURNS integer[]
 LANGUAGE sql
 STABLE
AS $function$
  WITH RECURSIVE up AS (
    SELECT id, parent_id FROM v3.expense_item WHERE id = ANY(p_eids)
    UNION
    SELECT p.id, p.parent_id
    FROM v3.expense_item p
    JOIN up u ON p.id = u.parent_id
  )
  SELECT array_agg(DISTINCT id) FROM up;
$function$
;

-- DROP FUNCTION v3._jsonb_diff(jsonb, jsonb);

CREATE OR REPLACE FUNCTION v3._jsonb_diff(a jsonb, b jsonb)
 RETURNS jsonb
 LANGUAGE plpgsql
 IMMUTABLE
AS $function$
DECLARE
  result JSONB := '{}'::jsonb;
  k TEXT;
  va JSONB;
  vb JSONB;
  sub JSONB;
BEGIN
  IF a IS NULL THEN
    RETURN COALESCE(b, '{}'::jsonb);
  END IF;
  IF b IS NULL THEN
    RETURN '{}'::jsonb;
  END IF;

  -- Ключи `b`: если отличаются от `a` — добавляем
  FOR k IN SELECT jsonb_object_keys(b) LOOP
    va := a -> k;
    vb := b -> k;
    IF jsonb_typeof(va) = 'object' AND jsonb_typeof(vb) = 'object' THEN
      sub := v3._jsonb_diff(va, vb);
      IF sub <> '{}'::jsonb THEN
        result := result || jsonb_build_object(k, sub);
      END IF;
    ELSIF va IS DISTINCT FROM vb THEN
      result := result || jsonb_build_object(k, vb);
    END IF;
  END LOOP;

  -- Ключи которые БЫЛИ в `a` но УДАЛЕНЫ в `b` — представляем как null
  FOR k IN SELECT jsonb_object_keys(a) LOOP
    IF NOT (b ? k) THEN
      result := result || jsonb_build_object(k, NULL);
    END IF;
  END LOOP;

  RETURN result;
END;
$function$
;

-- DROP FUNCTION v3._limit_template_path_set(_int4);

CREATE OR REPLACE FUNCTION v3._limit_template_path_set(p_ids integer[])
 RETURNS integer[]
 LANGUAGE sql
 STABLE
AS $function$
  WITH RECURSIVE up AS (
    SELECT id, parent_id FROM v3.limit_template WHERE id = ANY(p_ids)
    UNION
    SELECT p.id, p.parent_id
    FROM v3.limit_template p
    JOIN up u ON p.id = u.parent_id
  )
  SELECT array_agg(DISTINCT id) FROM up;
$function$
;

-- DROP FUNCTION v3.add_budget_form(varchar, int4, int4, int4);

CREATE OR REPLACE FUNCTION v3.add_budget_form(p_form_type character varying, p_year integer, p_org_unit_id integer DEFAULT NULL::integer, p_user_id integer DEFAULT NULL::integer)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_form_id INTEGER;
    v_ssp_list JSONB := '[]'::jsonb;
BEGIN
    IF p_user_id IS NOT NULL
       AND NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id) THEN
        RAISE EXCEPTION 'app_user id=% не существует', p_user_id;
    END IF;

    IF p_org_unit_id IS NOT NULL
       AND NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
        RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
    END IF;

    INSERT INTO v3.budget_form
        (form_type_code, year, org_unit_id, created_by)
    VALUES
        (p_form_type, p_year, p_org_unit_id, p_user_id)
    RETURNING id INTO v_form_id;

    PERFORM v3.copy_template_to_form(v_form_id);

    IF p_org_unit_id IS NOT NULL THEN
        SELECT jsonb_build_array(
            jsonb_build_object(
                'id', ou.id,
                'title', ou.title
            )
        )
        INTO v_ssp_list
        FROM v3.org_unit ou
        WHERE ou.id = p_org_unit_id;
    END IF;

    PERFORM v3.log_event(
        'CREATE_TASK', 'TASK',
        jsonb_build_object(
            'form_type',   p_form_type,
            'year',        p_year,
            'org_unit_id', p_org_unit_id,
            'ssp_list',    COALESCE(v_ssp_list, '[]'::jsonb),
            'entity_id', v_form_id
        ),
        v_form_id, null, p_org_unit_id
    );

    RETURN v_form_id;
END;
$function$
;

-- DROP FUNCTION v3.add_budget_line(int4, int4, varchar, varchar, varchar, varchar, varchar, varchar, int4, int4, varchar, varchar, date);

CREATE OR REPLACE FUNCTION v3.add_budget_line(p_form_id integer, p_expense_item_id integer DEFAULT NULL::integer, p_sheet character varying DEFAULT NULL::character varying, p_item_id character varying DEFAULT NULL::character varying, p_section_code character varying DEFAULT NULL::character varying, p_direction character varying DEFAULT NULL::character varying, p_name character varying DEFAULT NULL::character varying, p_internal_order character varying DEFAULT NULL::character varying, p_vsp_id integer DEFAULT NULL::integer, p_project_id integer DEFAULT NULL::integer, p_justification character varying DEFAULT NULL::character varying, p_contract_number character varying DEFAULT NULL::character varying, p_contract_end_date date DEFAULT NULL::date)
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_form_type    VARCHAR;
  v_org_id       INT;
  v_eid          INT;
  v_sheet        VARCHAR;
  v_auto_dir     VARCHAR;
  v_final_dir    VARCHAR;
  v_new_id       INT;
  v_cnt          INT;
  v_sat_table    TEXT;
  v_sat_eid      INT;
  v_parent_line  INT;
BEGIN
  -- 1. form_id → form_type
  SELECT form_type_code, org_unit_id INTO v_form_type, v_org_id FROM v3.budget_form WHERE id = p_form_id;
  IF v_form_type IS NULL THEN
    RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
  END IF;
  IF v_form_type = 'FORM_3' THEN
    RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)';
  END IF;

  -- ═══ Сателлитные листы (AHR_RENT/UTILITY/SECURITY) ═══════════════════
  -- Фиксированный expense_item_id на лист — UI не передаёт R-код.
  -- p_vsp_id обязателен. Опц.: p_contract_number, p_contract_end_date.
  -- Внутри: находим/создаём родительскую budget_line с фиксированным ei,
  --         затем INSERT в *_detail. Возврат — одна новая LEAF.
  IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
    v_sat_table := CASE p_sheet
      WHEN 'AHR_RENT'     THEN 'rent_detail'
      WHEN 'AHR_UTILITY'  THEN 'utility_detail'
      WHEN 'AHR_SECURITY' THEN 'security_detail'
    END;
    v_sat_eid := CASE p_sheet
      WHEN 'AHR_RENT'     THEN 293   -- R061031001 "Аренда_ОН"
      WHEN 'AHR_UTILITY'  THEN 24    -- 1.05.1.    "Коммунальные услуги"
      WHEN 'AHR_SECURITY' THEN 779   -- R061062001 "Пультовая охр.ОН"
    END;

    IF p_vsp_id IS NULL THEN
      RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet;
    END IF;
    IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
      RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
    END IF;

    -- Находим/создаём родительскую budget_line под фиксированным ei
    SELECT id INTO v_parent_line
      FROM v3.budget_line
     WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid
     LIMIT 1;
    IF v_parent_line IS NULL THEN
      INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction)
      VALUES (
        p_form_id, v_sat_eid,
        CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END
      ) RETURNING id INTO v_parent_line;
    END IF;

    -- INSERT в сателлит
    EXECUTE format(
      'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) '
      || 'VALUES ($1, $2, $3, $4) RETURNING id',
      v_sat_table
    ) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date
      INTO v_new_id;

    -- Возврат — одна новая INPUT-строка (отфильтрована по новому id сателлита)
    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v
     WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id;
    RETURN;
  END IF;

  -- 2. Резолв expense_item_id
  IF p_expense_item_id IS NOT NULL THEN
    SELECT ei.id, ei.sheet INTO v_eid, v_sheet
      FROM v3.expense_item ei WHERE ei.id = p_expense_item_id;
    IF v_eid IS NULL THEN
      RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
    END IF;
    IF NOT EXISTS (
      SELECT 1 FROM v3.expense_item_form_type
       WHERE expense_item_id = v_eid AND form_type_code = v_form_type
    ) THEN
      RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type;
    END IF;
  ELSE
    IF p_sheet IS NULL OR p_item_id IS NULL THEN
      RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)';
    END IF;

    SELECT count(*) INTO v_cnt
      FROM v3.expense_item ei
      JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
     WHERE ei.item_id = p_item_id
       AND ei.sheet   = p_sheet
       AND eift.form_type_code = v_form_type
       AND (p_section_code IS NULL OR ei.section_code = p_section_code)
       AND (p_direction    IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);

    IF v_cnt = 0 THEN
      RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%',
        p_sheet, p_item_id, p_section_code, p_direction, v_form_type;
    END IF;
    IF v_cnt > 1 THEN
      RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую',
        v_cnt, p_sheet, p_item_id, p_section_code, p_direction;
    END IF;

    SELECT ei.id, ei.sheet INTO v_eid, v_sheet
      FROM v3.expense_item ei
      JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
     WHERE ei.item_id = p_item_id
       AND ei.sheet   = p_sheet
       AND eift.form_type_code = v_form_type
       AND (p_section_code IS NULL OR ei.section_code = p_section_code)
       AND (p_direction    IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
  END IF;

  -- 3. Авто-direction (для FORM_1/AHR|CAP — обязателен у юзера)
  v_auto_dir := CASE
    WHEN v_form_type = 'FORM_2' THEN 'Support'
    WHEN v_form_type = 'FORM_4' THEN 'Development'
    WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL
    ELSE NULL
  END;

  IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN
    RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet;
  END IF;

  IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN
    RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)',
      p_direction, v_form_type, v_auto_dir;
  END IF;

  IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN
    RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction;
  END IF;

  v_final_dir := COALESCE(p_direction, v_auto_dir);

  -- 4. Sanity на vsp/project
  IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
    RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
  END IF;
  IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
    RAISE EXCEPTION 'project id=% не существует', p_project_id;
  END IF;

  -- 5. INSERT
  INSERT INTO v3.budget_line (
    budget_form_id, expense_item_id, name, internal_order,
    vsp_id, project_id, direction, justification
  ) VALUES (
    p_form_id, v_eid, p_name, p_internal_order,
    p_vsp_id, p_project_id, v_final_dir, p_justification
  ) RETURNING id INTO v_new_id;

  -- 5а. Аудит: ROW_CREATE
  PERFORM v3.log_event(
    'ROW_CREATE', 'ROW',
    jsonb_build_object(
      'sheet',           v_sheet,
      'line_id',         v_new_id,
      'expense_item_id', v_eid,
      'direction',       v_final_dir,
      'name',            p_name,
      'vsp_id',          p_vsp_id,
      'project_id',      p_project_id,
      'entity_id', v_new_id,
	  'form_id', p_form_id
    ),
    p_form_id, null, v_org_id
  );

  -- 6. Возврат: только иерархия по пути нового ei + новая INPUT
  DECLARE v_ei_path INT[];
  BEGIN
    v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
     ORDER BY v.sort_order;
  END;
END;
$function$
;

-- DROP FUNCTION v3.add_form3_line(int4, int4);

CREATE OR REPLACE FUNCTION v3.add_form3_line(p_report_id integer, p_expense_item_id integer)
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_new_id INT;
  v_project_id INT;
  v_org_id INT;
BEGIN
  SELECT project_id INTO v_project_id FROM v3.rf_project_report WHERE id = p_report_id;
    IF v_project_id IS NULL THEN
        RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
   
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.expense_item WHERE id = p_expense_item_id) THEN
    RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.expense_item_form_type
                  WHERE expense_item_id = p_expense_item_id AND form_type_code = 'FORM_3') THEN
    RAISE EXCEPTION 'expense_item id=% не привязан к form_type FORM_3', p_expense_item_id;
  END IF;

  INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
  VALUES (p_report_id, p_expense_item_id)
  RETURNING id INTO v_new_id;

  SELECT org_unit_id INTO v_org_id FROM v3.project WHERE id = v_project_id;



  -- Аудит: ROW_CREATE
  PERFORM v3.log_event(
    'ROW_CREATE', 'ROW',
    jsonb_build_object(
      'sheet',           'FORM_3',
      'line_id',         v_new_id,
      'report_id',       p_report_id,
      'expense_item_id', p_expense_item_id,
      'entity_id', v_new_id
    ), null, null, v_org_id
  );

  -- Возврат: только иерархия по пути нового ei + новая INPUT
  DECLARE v_ei_path INT[];
  BEGIN
    v_ei_path := v3._expense_item_path_set(ARRAY[p_expense_item_id]);
    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form3_report_jsonb(p_report_id, NULL) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
     ORDER BY v.sort_order;
  END;
END;
$function$
;

-- DROP FUNCTION v3.add_form_phase(int4, varchar, varchar, varchar, _text, timestamptz, timestamptz);

CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp with time zone, p_closes_at timestamp with time zone)
 RETURNS v3.form_phase
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_phase v3.form_phase;
    v_org_unit_id INT;
    v_org_unit_title VARCHAR;
    v_form_type_code VARCHAR;
    v_year INT;
    v_task_name TEXT;
    v_phase_id TEXT;
BEGIN
    INSERT INTO v3.form_phase(
        budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at
    )
    VALUES (
        p_budget_form_id, p_sheet, p_phase_code, p_role, p_column_keys, p_opens_at, p_closes_at
    )
    RETURNING * INTO v_phase;

    SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
      INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
      FROM v3.budget_form bf
      LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
     WHERE bf.id = p_budget_form_id;
    v_task_name := trim(concat(
        COALESCE(v_form_type_code, ''),
        CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
        CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
    ));
    v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);

    PERFORM v3.log_event(
        'ACCESS_WINDOW_CHANGE', 'ACCESS',
        jsonb_build_object(
            'entity_type', 'form_phase',
            'entity_id', p_budget_form_id,
            'action', 'create',
            'phase_id', v_phase_id,
            'phase_name', p_phase_code,
            'task_id', p_budget_form_id,
            'form_id', p_budget_form_id,
            'budget_form_id', p_budget_form_id,
            'org_unit_id', v_org_unit_id,
            'org_unit_name', v_org_unit_title,
            'sheet', p_sheet,
            'phase_code', p_phase_code,
            'role', v_phase.role,
            'column_keys', v_phase.column_keys,
            'opens_at', v_phase.opens_at,
            'closes_at', v_phase.closes_at
        ),
        p_budget_form_id, null, v_org_unit_id
    );

    RETURN v_phase;
END;
$function$
;

-- DROP FUNCTION v3.add_project(varchar, int4, int4, varchar, int4, varchar, varchar, varchar, varchar, int4, numeric);

CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric)
 RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_pid INT;
  v_lim INT;
  v_cur INT;
BEGIN
  IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
    RAISE EXCEPTION 'p_name обязателен';
  END IF;
  IF p_year IS NULL THEN
    RAISE EXCEPTION 'p_year обязателен';
  END IF;
  IF p_org_unit_id IS NULL THEN
    RAISE EXCEPTION 'p_org_unit_id обязателен';
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
    RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
  END IF;
  IF p_parent_id IS NOT NULL AND
     NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
    RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
  END IF;

  -- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type
  -- из migrate_form3_v1.sql применятся автоматически).
  INSERT INTO v3.project (
    name, level, parent_id, org_unit_id,
    project_type, vsp_format, placement_type, object_address, staff_count, total_area
  ) VALUES (
    p_name, p_level, p_parent_id, p_org_unit_id,
    p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area
  ) RETURNING id INTO v_pid;

  -- Создаём 2 отчёта (LIMIT + CURRENT_EXPENSES) для проекта × год
  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (v_pid, p_year, 'LIMIT')            RETURNING id INTO v_lim;
  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (v_pid, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur;

  -- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
  PERFORM v3.log_event(
    'PROJECT_CREATE', 'PROJECT',
    jsonb_strip_nulls(jsonb_build_object(
      'project_id',     v_pid,
      'name',           p_name,
      'year',           p_year,
      'level',          p_level,
      'parent_id',      p_parent_id,
      'org_unit_id',    p_org_unit_id,
      'project_type',   p_project_type,
      'vsp_format',     p_vsp_format,
      'placement_type', p_placement_type,
      'object_address', p_object_address,
      'staff_count',    p_staff_count,
      'total_area',     p_total_area,
      'limit_report_id',            v_lim,
      'current_expenses_report_id', v_cur,
      'entity_id', v_pid
    )), null, null, p_org_unit_id
  );

  RETURN QUERY SELECT v_pid, v_lim, v_cur;
END;
$function$
;

-- DROP FUNCTION v3.add_user(varchar, varchar, varchar, varchar, int4);

CREATE OR REPLACE FUNCTION v3.add_user(p_email character varying, p_username character varying, p_hashed_password character varying, p_full_name character varying, p_role_id integer)
 RETURNS v3.app_user
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_user v3.app_user;
BEGIN
    INSERT INTO v3.app_user(email, username, hashed_password, full_name, role_id, is_active)
    VALUES (p_email, p_username, p_hashed_password, p_full_name, p_role_id, TRUE)
    RETURNING * INTO v_user;

    PERFORM v3.log_event(
        'USER_CREATE', 'USER',
        jsonb_strip_nulls(jsonb_build_object(
            'user_id',   v_user.id,
            'email',     v_user.email,
            'username',  v_user.username,
            'full_name', v_user.full_name,
            'role_id',   v_user.role_id,
            'entity_id', v_user.id
        ))
    );

    RETURN v_user;
END;
$function$
;

-- DROP FUNCTION v3.add_vsp(int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, date);

CREATE OR REPLACE FUNCTION v3.add_vsp(p_branch_id integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT true, p_is_deleted boolean DEFAULT false, p_system_code character varying DEFAULT NULL::character varying, p_created_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_id INT;
    v_branch_name VARCHAR;
    v_actor_id INT;
    v_actor_email VARCHAR;
    v_actor_full_name VARCHAR;
BEGIN
    SELECT ou.title INTO v_branch_name
    FROM v3.org_unit ou
    WHERE ou.id = p_branch_id;

    v_actor_id := COALESCE(p_created_by, v3.current_user_id());

    IF v_actor_id IS NOT NULL THEN
        SELECT u.email, u.full_name
          INTO v_actor_email, v_actor_full_name
        FROM v3.app_user u
        WHERE u.id = v_actor_id;
    END IF;

    INSERT INTO v3.vsp
        (branch_id, reg_number, address, format, opened_at,
         placement_type, staff_count, total_area, closed_at,
         is_active, is_deleted, system_code, created_by,
         vsp_type, notes, rent_contract_num, rent_end_date)
    VALUES
        (p_branch_id, p_reg_number, p_address, p_format, p_opened_at,
         p_placement_type, p_staff_count, p_total_area, p_closed_at,
         p_is_active, p_is_deleted, p_system_code, p_created_by,
         p_vsp_type, p_notes, p_rent_contract_num, p_rent_end_date)
    RETURNING id INTO v_id;

    PERFORM v3.log_event(
        'VSP_CREATE', 'VSP',
        jsonb_build_object(
            'vsp_id',            v_id,
            'entity_id',         v_id,
            'branch_id',         p_branch_id,
            'branch_name',       v_branch_name,
            'reg_number',        p_reg_number,
            'address',           p_address,
            'format',            p_format,
            'opened_at',         v3._diff_val(p_opened_at),
            'placement_type',    p_placement_type,
            'staff_count',       p_staff_count,
            'total_area',        v3._diff_val(p_total_area),
            'closed_at',         v3._diff_val(p_closed_at),
            'is_active',         p_is_active,
            'is_deleted',        p_is_deleted,
            'system_code',       p_system_code,
            'created_by',        p_created_by,
            'vsp_type',          p_vsp_type,
            'notes',             p_notes,
            'rent_contract_num', p_rent_contract_num,
            'rent_end_date',     v3._diff_val(p_rent_end_date),
            'user_email',        v_actor_email,
            'user_full_name',    v_actor_full_name
        ), null, null, p_branch_id
    );

    RETURN v_id;
END;
$function$
;

-- DROP FUNCTION v3.can_edit(int4, varchar, text, int4);

CREATE OR REPLACE FUNCTION v3.can_edit(p_form_id integer, p_sheet character varying, p_column_key text, p_user_id integer)
 RETURNS jsonb
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    v_role         VARCHAR;
    v_active_close TIMESTAMPTZ;
    v_role_has_col BOOLEAN;
    v_next_open    TIMESTAMPTZ;
    v_last_close   TIMESTAMPTZ;
BEGIN
    IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN
        RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
    END IF;

    v_role := v3.user_role_code(p_user_id);
    IF v_role IS NULL THEN
        RETURN jsonb_build_object(
            'ok',     false,
            'code',   'role_not_allowed',
            'detail', 'unknown or inactive user'
        );
    END IF;

    -- ADMIN — bypass всех проверок.
    IF v_role = 'ADMIN' THEN
        RETURN jsonb_build_object('ok', true, 'admin', true);
    END IF;

    -- EXECUTOR_RF: фильтр по user_org формы.
    IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_form_org(p_user_id, p_form_id) THEN
        RETURN jsonb_build_object(
            'ok',     false,
            'code',   'org_not_assigned',
            'detail', p_column_key
        );
    END IF;

    -- 1. Активная сейчас фаза, в которой эта колонка перечислена для этой роли?
    SELECT MAX(fp.closes_at)
      INTO v_active_close
      FROM v3.form_phase fp
     WHERE fp.budget_form_id = p_form_id
       AND fp.sheet          = p_sheet
       AND fp.role           = v_role
       AND p_column_key      = ANY(fp.column_keys)
       AND now() BETWEEN fp.opens_at AND fp.closes_at;

    IF v_active_close IS NOT NULL THEN
        RETURN jsonb_build_object(
            'ok',        true,
            'closes_at', v_active_close
        );
    END IF;

    -- 2. Колонка В ПРИНЦИПЕ есть хоть в одной фазе этой роли (на этой форме/листе)?
    --    Если нет — это вопрос whitelist, отдаём role_not_allowed.
    SELECT EXISTS (
        SELECT 1
          FROM v3.form_phase fp
         WHERE fp.budget_form_id = p_form_id
           AND fp.sheet          = p_sheet
           AND fp.role           = v_role
           AND p_column_key      = ANY(fp.column_keys)
    ) INTO v_role_has_col;

    IF NOT v_role_has_col THEN
        RETURN jsonb_build_object(
            'ok',     false,
            'code',   'role_not_allowed',
            'detail', p_column_key
        );
    END IF;

    -- 3. Колонка доступна, но окно не активно. Подсказать ближайшее.
    SELECT MIN(fp.opens_at)
      INTO v_next_open
      FROM v3.form_phase fp
     WHERE fp.budget_form_id = p_form_id
       AND fp.sheet          = p_sheet
       AND fp.role           = v_role
       AND p_column_key      = ANY(fp.column_keys)
       AND fp.opens_at       > now();

    IF v_next_open IS NOT NULL THEN
        RETURN jsonb_build_object(
            'ok',       false,
            'code',     'window_closed',
            'detail',   p_column_key,
            'opens_at', v_next_open
        );
    END IF;

    SELECT MAX(fp.closes_at)
      INTO v_last_close
      FROM v3.form_phase fp
     WHERE fp.budget_form_id = p_form_id
       AND fp.sheet          = p_sheet
       AND fp.role           = v_role
       AND p_column_key      = ANY(fp.column_keys)
       AND fp.closes_at     <= now();

    RETURN jsonb_build_object(
        'ok',        false,
        'code',      'window_closed',
        'detail',    p_column_key,
        'closes_at', v_last_close
    );
END;
$function$
;

-- DROP FUNCTION v3.copy_template_to_form(int4);

CREATE OR REPLACE FUNCTION v3.copy_template_to_form(p_form_id integer)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_form_type VARCHAR;
    v_inserted  INTEGER;
BEGIN
    SELECT form_type_code INTO v_form_type
      FROM v3.budget_form
     WHERE id = p_form_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
    END IF;

    INSERT INTO v3.form_phase
        (budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at)
    SELECT p_form_id, pt.sheet, pt.phase_code, pt.role,
           pt.column_keys, pt.opens_at, pt.closes_at
      FROM v3.phase_template pt
     WHERE pt.form_type = v_form_type
    ON CONFLICT (budget_form_id, sheet, phase_code) DO NOTHING;

    GET DIAGNOSTICS v_inserted = ROW_COUNT;
    RETURN v_inserted;
END;
$function$
;

-- DROP FUNCTION v3.current_user_id();

CREATE OR REPLACE FUNCTION v3.current_user_id()
 RETURNS integer
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    v_raw TEXT;
    v_id  INTEGER;
BEGIN
    v_raw := current_setting('app.user_id', true);
    IF v_raw IS NULL OR v_raw = '' THEN
        RETURN NULL;
    END IF;
    BEGIN
        v_id := v_raw::INTEGER;
    EXCEPTION WHEN OTHERS THEN
        RETURN NULL;
    END;
    RETURN v_id;
END;
$function$
;

-- DROP FUNCTION v3.deactivate_vsp(int4);

CREATE OR REPLACE FUNCTION v3.deactivate_vsp(p_vsp_id integer)
 RETURNS date
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_closed DATE;
    v_old_closed DATE;
    v_branch_id INT;
    v_branch_name VARCHAR;
    v_actor_id INT;
    v_actor_email VARCHAR;
    v_actor_full_name VARCHAR;
BEGIN
    SELECT v.branch_id, v.closed_at, ou.title
      INTO v_branch_id, v_old_closed, v_branch_name
      FROM v3.vsp v
      LEFT JOIN v3.org_unit ou ON ou.id = v.branch_id
     WHERE v.id = p_vsp_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
    END IF;
    IF v_old_closed IS NOT NULL THEN
        RAISE EXCEPTION 'vsp #% уже закрыт', p_vsp_id;
    END IF;

    UPDATE v3.vsp SET closed_at = CURRENT_DATE
     WHERE id = p_vsp_id
    RETURNING closed_at INTO v_closed;

    v_actor_id := v3.current_user_id();
    IF v_actor_id IS NOT NULL THEN
        SELECT u.email, u.full_name
          INTO v_actor_email, v_actor_full_name
          FROM v3.app_user u
         WHERE u.id = v_actor_id;
    END IF;

    PERFORM v3.log_event(
        'VSP_UPDATE', 'VSP',
        jsonb_build_object(
            'vsp_id',    p_vsp_id,
            'branch_id', v_branch_id,
            'branch_name', v_branch_name,
            'changes', jsonb_build_object(
                'closed_at', jsonb_build_object(
                    'before', v3._diff_val(v_old_closed),
                    'after', v3._diff_val(v_closed)
                )
            ),
            'closed_at', v_closed::TEXT,
            'user_id', v_actor_id,
            'user_email', v_actor_email,
            'user_full_name', v_actor_full_name
        ), null, null, v_branch_id);

    RETURN v_closed;
END;
$function$
;

-- DROP FUNCTION v3.del_budget_form(int4);

CREATE OR REPLACE FUNCTION v3.del_budget_form(p_form_id integer)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old RECORD;
BEGIN
    SELECT bf.form_type_code, bf.year, bf.org_unit_id
      INTO v_old
      FROM v3.budget_form bf WHERE bf.id = p_form_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'budget_form #% не существует', p_form_id;
    END IF;

    DELETE FROM v3.budget_form WHERE id = p_form_id;

    PERFORM v3.log_event(
        'DELETE_TASK', 'TASK',
        jsonb_build_object(
            'form_id',     p_form_id,
            'form_type',   v_old.form_type_code,
            'year',        v_old.year,
            'org_unit_id', v_old.org_unit_id),
        p_form_id, null, v_old.org_unit_id);

    RETURN p_form_id;
END;
$function$
;

-- DROP FUNCTION v3.del_budget_line(int4, varchar, varchar);

CREATE OR REPLACE FUNCTION v3.del_budget_line(p_id integer, p_direction character varying DEFAULT NULL::character varying, p_sheet character varying DEFAULT NULL::character varying)
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_form_id    INT;
  v_org_id    INT;
  v_sheet      VARCHAR;
  v_sat_table  TEXT;
  v_eid        INT;
  v_ei_path    INT[];
BEGIN
  -- ═══ Сателлитные листы — DELETE из *_detail по id ═════════════════════
  IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
    v_sat_table := CASE p_sheet
      WHEN 'AHR_RENT'     THEN 'rent_detail'
      WHEN 'AHR_UTILITY'  THEN 'utility_detail'
      WHEN 'AHR_SECURITY' THEN 'security_detail'
    END;

    -- Извлекаем form_id для возврата (через JOIN на budget_line)
    EXECUTE format(
      'SELECT bl.budget_form_id FROM v3.%I s
        JOIN v3.budget_line bl ON bl.id = s.line_id
        WHERE s.id = $1', v_sat_table
    ) USING p_id INTO v_form_id;

    IF v_form_id IS NULL THEN
      RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_id;
    END IF;

    EXECUTE format('DELETE FROM v3.%I WHERE id = $1', v_sat_table) USING p_id;

    -- Возврат пустой — UI сам уберёт строку по id
    RETURN;
  END IF;

  -- ═══ Основная сетка ═══════════════════════════════════════════════════
  -- Sanity + извлечение form_id/sheet/expense_item_id (нужен для path
  -- ДО DELETE — после удаления budget_line его уже не получить).
  SELECT bl.budget_form_id, ei.sheet, bl.expense_item_id
    INTO v_form_id, v_sheet, v_eid
  FROM v3.budget_line bl
  JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
  WHERE bl.id = p_id;

  IF v_form_id IS NULL THEN
    RAISE EXCEPTION 'budget_line id=% не существует', p_id;
  END IF;

  v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);

  -- Каскадное удаление дочерних записей
  DELETE FROM v3.plan                WHERE line_id = p_id;
  DELETE FROM v3.sequestration       WHERE line_id = p_id;
  DELETE FROM v3.reserve             WHERE line_id = p_id;
  DELETE FROM v3.allocation          WHERE line_id = p_id;
  DELETE FROM v3.contract_summary    WHERE line_id = p_id;
  DELETE FROM v3.collegial_approval  WHERE line_id = p_id;
  DELETE FROM v3.ckk                 WHERE line_id = p_id;
  DELETE FROM v3.contract_detail     WHERE line_id = p_id;
  DELETE FROM v3.booking             WHERE line_id = p_id;
  DELETE FROM v3.budget_line_quarter WHERE line_id = p_id;
  DELETE FROM v3.rent_detail         WHERE line_id = p_id;
  DELETE FROM v3.utility_detail      WHERE line_id = p_id;
  DELETE FROM v3.security_detail     WHERE line_id = p_id;
  -- AHR_LIMIT с v3 не имеет привязки к budget_line — каталог limit_template
  -- глобальный, per-form qty в form_limit удаляется через ON DELETE CASCADE
  -- от budget_form.

  -- Удаляем сам budget_line
  DELETE FROM v3.budget_line WHERE id = p_id;

  SELECT org_unit_id INTO v_org_id FROM v3.budget_form WHERE id = v_form_id;

  -- Аудит: ROW_DELETE
  PERFORM v3.log_event(
    'ROW_DELETE', 'ROW',
    jsonb_build_object(
      'sheet',           v_sheet,
      'line_id',         p_id,
      'entity_id', p_id,
      'expense_item_id', v_eid,
      'direction',       p_direction
    ),
    v_form_id, NULL, v_org_id
  );

  -- Возврат: иерархия только по пути удалённой строки (INPUT уже нет)
  RETURN QUERY
  SELECT v.row_type, v.depth, v.sort_order, v.data
    FROM v3.v_form_view(v_form_id, v_sheet, NULL, p_direction) v
   WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
     AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
   ORDER BY v.sort_order;
END;
$function$
;

-- DROP FUNCTION v3.del_form3_line(int4);

CREATE OR REPLACE FUNCTION v3.del_form3_line(p_line_id integer)
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_report_id INT;
  v_eid       INT;
  v_form_id   INT;
  v_ei_path   INT[];
BEGIN
  -- Извлекаем report_id + ei ДО DELETE (нужен для path)
  SELECT rfprl.rf_project_report_id, rfprl.expense_item_id
    INTO v_report_id, v_eid
    FROM v3.rf_project_report_line rfprl
   WHERE rfprl.id = p_line_id;
  IF v_report_id IS NULL THEN
    RAISE EXCEPTION 'rf_project_report_line id=% не существует', p_line_id;
  END IF;

  v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);

  SELECT bf.id
    INTO v_form_id
    FROM v3.rf_project_report r
    JOIN v3.project p
      ON p.id = r.project_id
    LEFT JOIN v3.budget_form bf
      ON bf.form_type_code = 'FORM_3'
     AND bf.year = r.year
     AND bf.org_unit_id = p.org_unit_id
   WHERE r.id = v_report_id
   LIMIT 1;

  -- Каскад (FK без ON DELETE CASCADE)
  DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id = p_line_id;
  DELETE FROM v3.rf_project_report_line    WHERE id = p_line_id;

  -- Аудит: ROW_DELETE
  PERFORM v3.log_event(
    'ROW_DELETE', 'ROW',
    jsonb_build_object(
      'sheet',           'FORM_3',
      'line_id',         p_line_id,
      'form_id',         v_form_id,
      'report_id',       v_report_id,
      'expense_item_id', v_eid
    ),
    v_form_id
  );

  -- Возврат: только иерархия по пути удалённой (INPUT уже нет)
  RETURN QUERY
  SELECT v.row_type, v.depth, v.sort_order, v.data
    FROM v3.v_form3_report_jsonb(v_report_id, NULL) v
   WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
     AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
   ORDER BY v.sort_order;
END;
$function$
;

-- DROP FUNCTION v3.del_form_phase(int4, varchar, varchar);

CREATE OR REPLACE FUNCTION v3.del_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old v3.form_phase;
    v_org_unit_id INT;
    v_org_unit_title VARCHAR;
    v_form_type_code VARCHAR;
    v_year INT;
    v_task_name TEXT;
    v_phase_id TEXT;
BEGIN
    DELETE FROM v3.form_phase
     WHERE budget_form_id = p_budget_form_id
       AND sheet = p_sheet
       AND phase_code = p_phase_code
     RETURNING * INTO v_old;

    IF NOT FOUND THEN
        RETURN FALSE;
    END IF;

    SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
      INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
      FROM v3.budget_form bf
      LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
     WHERE bf.id = p_budget_form_id;
    v_task_name := trim(concat(
        COALESCE(v_form_type_code, ''),
        CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
        CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
    ));
    v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);

    PERFORM v3.log_event(
        'ACCESS_WINDOW_CHANGE', 'ACCESS',
        jsonb_build_object(
            'entity_type', 'form_phase',
            'entity_id', p_budget_form_id,
            'action', 'delete',
            'phase_id', v_phase_id,
            'phase_name', p_phase_code,
            'task_id', p_budget_form_id,
            'task_name', NULLIF(v_task_name, ''),
            'form_id', p_budget_form_id,
            'budget_form_id', p_budget_form_id,
            'org_unit_id', v_org_unit_id,
            'org_unit_name', v_org_unit_title,
            'sheet', p_sheet,
            'phase_code', p_phase_code,
            'role', v_old.role,
            'column_keys', v_old.column_keys,
            'opens_at', v_old.opens_at,
            'closes_at', v_old.closes_at
        ),
        p_budget_form_id, null, v_org_unit_id
    );
    RETURN TRUE;
END;
$function$
;

-- DROP FUNCTION v3.del_project(int4);

CREATE OR REPLACE FUNCTION v3.del_project(p_project_id integer)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old RECORD;
BEGIN
    SELECT p.name, p.level, p.org_unit_id
      INTO v_old
      FROM v3.project p WHERE p.id = p_project_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'project #% не существует', p_project_id;
    END IF;

    -- Каскад вручную: rf_project_report_quarter / line / report (нет ON DELETE)
    DELETE FROM v3.rf_project_report_quarter
     WHERE rf_project_report_line_id IN (
        SELECT l.id FROM v3.rf_project_report_line l
          JOIN v3.rf_project_report r ON r.id = l.rf_project_report_id
         WHERE r.project_id = p_project_id);
    DELETE FROM v3.rf_project_report_line
     WHERE rf_project_report_id IN (
        SELECT id FROM v3.rf_project_report WHERE project_id = p_project_id);
    DELETE FROM v3.rf_project_report WHERE project_id = p_project_id;
    DELETE FROM v3.project WHERE id = p_project_id;

    PERFORM v3.log_event(
        'PROJECT_DELETE', 'PROJECT',
        jsonb_build_object(
            'project_id',  p_project_id,
            'name',        v_old.name,
            'level',       v_old.level,
            'org_unit_id', v_old.org_unit_id), null, null, v_old.org_unit_id);

    RETURN p_project_id;
END;
$function$
;

-- DROP FUNCTION v3.del_vsp(int4, int4);

CREATE OR REPLACE FUNCTION v3.del_vsp(p_vsp_id integer, p_deleted_by integer)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old RECORD;
    v_usr RECORD;
    v_closed DATE;
BEGIN
    SELECT vsp.id, vsp.branch_id
      INTO v_old
      FROM v3.vsp vsp WHERE vsp.id = p_vsp_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
    END IF;
	SELECT usr.id
      INTO v_usr
      FROM v3.app_user usr WHERE usr.id = p_deleted_by;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'user #% не существует', p_deleted_by;
    END IF;


	UPDATE v3.vsp SET is_deleted = true, updated_by = p_deleted_by WHERE id = p_vsp_id;
    UPDATE v3.vsp SET closed_at = CURRENT_DATE
     WHERE id = p_vsp_id AND closed_at IS NULL
    RETURNING closed_at INTO v_closed;

    IF v_closed IS NULL THEN
        IF EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
            RAISE EXCEPTION 'vsp #% уже закрыт', p_vsp_id;
        ELSE
            RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
        END IF;
    END IF;

    PERFORM v3.log_event(
        'DELETE_VSP', 'VSP',
        jsonb_build_object(
            'vsp_id',     p_vsp_id,
            'entity_id',     p_vsp_id,
            'branch_id',   v_old.branch_id,
			'updated_by', p_deleted_by
),
--vsp_id,
null, null, v_old.branch_id);

    RETURN p_vsp_id;
END;
$function$
;

-- DROP FUNCTION v3.editable_columns_for(int4, int4, varchar);

CREATE OR REPLACE FUNCTION v3.editable_columns_for(p_form_id integer, p_user_id integer, p_sheet character varying)
 RETURNS TABLE(column_key text, closes_at timestamp with time zone)
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    v_role VARCHAR;
BEGIN
    IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN
        RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
    END IF;

    v_role := v3.user_role_code(p_user_id);
    IF v_role IS NULL THEN
        RETURN;          -- неизвестный/неактивный юзер → пустая маска
    END IF;

    -- ADMIN: вернуть UNION всех column_keys всех phase'ов на форме/листе.
    IF v_role = 'ADMIN' THEN
        RETURN QUERY
        SELECT ck                 AS column_key,
               MAX(fp.closes_at)  AS closes_at
          FROM v3.form_phase fp,
               LATERAL unnest(fp.column_keys) AS ck
         WHERE fp.budget_form_id = p_form_id
           AND fp.sheet          = p_sheet
         GROUP BY ck;
        RETURN;
    END IF;

    -- EXECUTOR_RF без назначения → пустая маска (default deny).
    IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_form_org(p_user_id, p_form_id) THEN
        RETURN;
    END IF;

    RETURN QUERY
    SELECT ck                        AS column_key,
           MAX(fp.closes_at)         AS closes_at
      FROM v3.form_phase fp,
           LATERAL unnest(fp.column_keys) AS ck
     WHERE fp.budget_form_id = p_form_id
       AND fp.sheet          = p_sheet
       AND fp.role           = v_role
       AND now() BETWEEN fp.opens_at AND fp.closes_at
     GROUP BY ck;
END;
$function$
;

-- DROP FUNCTION v3.grant_user_org_access(int4, int4);

CREATE OR REPLACE FUNCTION v3.grant_user_org_access(p_user_id integer, p_org_unit_id integer)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_inserted INT;
    v_user_email VARCHAR;
    v_user_full_name VARCHAR;
    v_org_name VARCHAR;
BEGIN
    SELECT email, full_name
      INTO v_user_email, v_user_full_name
      FROM v3.app_user
     WHERE id = p_user_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'app_user #% не существует', p_user_id;
    END IF;

    SELECT title INTO v_org_name FROM v3.org_unit WHERE id = p_org_unit_id;
    IF v_org_name IS NULL THEN
        RAISE EXCEPTION 'org_unit #% не существует', p_org_unit_id;
    END IF;

    INSERT INTO v3.user_org(user_id, org_unit_id)
    VALUES (p_user_id, p_org_unit_id)
    ON CONFLICT (user_id, org_unit_id) DO NOTHING;

    GET DIAGNOSTICS v_inserted = ROW_COUNT;

    IF v_inserted > 0 THEN
        PERFORM v3.log_event(
            'USER_ACCESS_GRANTED', 'USER',
            jsonb_build_object(
                'user_id', p_user_id,
                'entity_id', p_user_id,
                'email', v_user_email,
                'full_name', v_user_full_name,
                'org_unit_id', p_org_unit_id,
                'org_unit_name', v_org_name
            ), null, null, p_org_unit_id
        );
        RETURN TRUE;
    END IF;

    RETURN FALSE;
END;
$function$
;

-- DROP FUNCTION v3.jsonb_numeric_sum(jsonb, jsonb);

CREATE OR REPLACE FUNCTION v3.jsonb_numeric_sum(a jsonb, b jsonb)
 RETURNS jsonb
 LANGUAGE sql
 IMMUTABLE
AS $function$
  SELECT CASE
    WHEN a IS NULL THEN b
    WHEN b IS NULL THEN a
    WHEN jsonb_typeof(a) = 'number' AND jsonb_typeof(b) = 'number' THEN
      to_jsonb((a)::text::numeric + (b)::text::numeric)
    WHEN jsonb_typeof(a) = 'object' AND jsonb_typeof(b) = 'object' THEN (
      SELECT jsonb_object_agg(k,
        v3.jsonb_numeric_sum(a -> k, b -> k)
      )
      FROM (
        SELECT k FROM jsonb_object_keys(a) k
        UNION
        SELECT k FROM jsonb_object_keys(b) k
      ) keys
    )
    -- одна сторона число/объект, другая нет → берём числовую/объектную
    WHEN jsonb_typeof(a) IN ('number','object') THEN a
    WHEN jsonb_typeof(b) IN ('number','object') THEN b
    ELSE NULL
  END;
$function$
;

-- DROP AGGREGATE v3.jsonb_sum(jsonb);

CREATE OR REPLACE AGGREGATE v3.jsonb_sum(pg_catalog.jsonb) (
	SFUNC = v3.jsonb_numeric_sum,
	STYPE = jsonb
);

-- DROP FUNCTION v3.log_event(text, text, jsonb, int4, int4, int4);

CREATE OR REPLACE FUNCTION v3.log_event(p_event text, p_event_type text, p_data jsonb, p_form_id integer DEFAULT NULL::integer, p_task_id integer DEFAULT NULL::integer, p_org_unit_id integer DEFAULT NULL::integer)
 RETURNS bigint
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_user_id  INTEGER;
    v_audit_id BIGINT;
BEGIN
    v_user_id := v3.current_user_id();

    -- Невалидный user_id → пишем NULL (не падаем).
    IF v_user_id IS NOT NULL
       AND NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = v_user_id) THEN
        v_user_id := NULL;
    END IF;

    INSERT INTO v3.audit_log
        (user_id, org_unit_id, form_id, task_id, event, event_type, event_data)
    VALUES
        (v_user_id, p_org_unit_id, p_form_id, p_task_id,
         p_event, p_event_type, COALESCE(p_data, '{}'::jsonb))
    RETURNING id INTO v_audit_id;

    RETURN v_audit_id;
END;
$function$
;

-- DROP FUNCTION v3.revoke_many_user_org_access(int4, _int4);

CREATE OR REPLACE FUNCTION v3.revoke_many_user_org_access(p_user_id integer, p_org_unit_ids integer[])
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_org_id INT;
    v_count  INT := 0;
BEGIN
    IF p_org_unit_ids IS NULL OR cardinality(p_org_unit_ids) = 0 THEN
        RETURN 0;
    END IF;

    FOREACH v_org_id IN ARRAY p_org_unit_ids
    LOOP
        IF v3.revoke_user_org_access(p_user_id, v_org_id) THEN
            v_count := v_count + 1;
        END IF;
    END LOOP;

    RETURN v_count;
END;
$function$
;

-- DROP FUNCTION v3.revoke_user_org_access(int4, int4);

CREATE OR REPLACE FUNCTION v3.revoke_user_org_access(p_user_id integer, p_org_unit_id integer)
 RETURNS boolean
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_deleted  INT;
    v_user_email VARCHAR;
    v_user_full_name VARCHAR;
    v_org_name VARCHAR;
BEGIN
    SELECT email, full_name
      INTO v_user_email, v_user_full_name
      FROM v3.app_user
     WHERE id = p_user_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'app_user #% не существует', p_user_id;
    END IF;

    SELECT title INTO v_org_name FROM v3.org_unit WHERE id = p_org_unit_id;
    IF v_org_name IS NULL THEN
        RAISE EXCEPTION 'org_unit #% не существует', p_org_unit_id;
    END IF;

    DELETE FROM v3.user_org
     WHERE user_id = p_user_id
       AND org_unit_id = p_org_unit_id;

    GET DIAGNOSTICS v_deleted = ROW_COUNT;

    IF v_deleted > 0 THEN
        PERFORM v3.log_event(
            'USER_ACCESS_REVOKED', 'USER',
            jsonb_build_object(
                'user_id', p_user_id,
                'entity_id', p_user_id,
                'email', v_user_email,
                'full_name', v_user_full_name,
                'org_unit_id', p_org_unit_id,
                'org_unit_name', v_org_name
            ), null, null, p_org_unit_id
        );
        RETURN TRUE;
    END IF;

    RETURN FALSE;
END;
$function$
;

-- DROP FUNCTION v3.upd_budget_form(int4, int4, int4);

CREATE OR REPLACE FUNCTION v3.upd_budget_form(p_form_id integer, p_year integer DEFAULT NULL::integer, p_org_unit_id integer DEFAULT NULL::integer)
 RETURNS TABLE(id integer, form_type_code character varying, year integer, org_unit_id integer)
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old     RECORD;
    v_changes JSONB := '{}'::jsonb;
BEGIN
    SELECT bf.year, bf.org_unit_id
      INTO v_old
      FROM v3.budget_form bf WHERE bf.id = p_form_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'budget_form #% не существует', p_form_id;
    END IF;

    IF p_year IS NOT NULL AND p_year IS DISTINCT FROM v_old.year THEN
        UPDATE v3.budget_form bf SET year = p_year WHERE bf.id = p_form_id;
        v_changes := v_changes || jsonb_build_object(
            'year', jsonb_build_object(
                'before', v3._diff_val(v_old.year),
                'after',  v3._diff_val(p_year)));
    END IF;

    IF p_org_unit_id IS NOT NULL AND p_org_unit_id IS DISTINCT FROM v_old.org_unit_id THEN
        UPDATE v3.budget_form bf SET org_unit_id = p_org_unit_id WHERE bf.id = p_form_id;
        v_changes := v_changes || jsonb_build_object(
            'org_unit_id', jsonb_build_object(
                'before', v3._diff_val(v_old.org_unit_id),
                'after',  v3._diff_val(p_org_unit_id)));
    END IF;

    IF v_changes <> '{}'::jsonb THEN
        UPDATE v3.budget_form bf SET updated_at = now() WHERE bf.id = p_form_id;
        PERFORM v3.log_event(
            'UPDATE_TASK', 'UPDATE_TASK',
            jsonb_build_object('form_id', p_form_id, 'changes', v_changes),
            p_form_id, null, COALESCE(p_org_unit_id, v_old.org_unit_id));
    END IF;

    RETURN QUERY
    SELECT bf.id, bf.form_type_code, bf.year, bf.org_unit_id
      FROM v3.budget_form bf WHERE bf.id = p_form_id;
END;
$function$
;

-- DROP FUNCTION v3.upd_form3_cells(int4, jsonb);

CREATE OR REPLACE FUNCTION v3.upd_form3_cells(p_report_id integer, p_changes jsonb)
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_change         JSONB;
  v_line_id        INT;
  v_column         TEXT;
  v_value          JSONB;
  v_changed_lines  INT[] := ARRAY[]::INT[];
BEGIN
  IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
    RAISE EXCEPTION 'p_changes must be a JSONB array';
  END IF;

  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
  LOOP
    v_line_id := (v_change->>'line_id')::INT;
    v_column  := v_change->>'column';
    v_value   := v_change->'value';

    IF v_line_id IS NULL OR v_column IS NULL THEN
      RAISE EXCEPTION 'change must have line_id and column: %', v_change;
    END IF;

    IF NOT EXISTS (
      SELECT 1 FROM v3.rf_project_report_line
       WHERE id = v_line_id AND rf_project_report_id = p_report_id
    ) THEN
      RAISE EXCEPTION 'rf_project_report_line id=% не принадлежит report id=%',
        v_line_id, p_report_id;
    END IF;

    PERFORM v3._apply_form3_cell(v_line_id, v_column, v_value);

    IF NOT (v_line_id = ANY(v_changed_lines)) THEN
      v_changed_lines := v_changed_lines || v_line_id;
    END IF;
  END LOOP;

  -- Возврат: иерархия по пути изменённых rfprl + изменённые INPUT по line_id
  DECLARE
    v_eid_set INT[];
    v_ei_path INT[];
  BEGIN
    SELECT array_agg(DISTINCT rfprl.expense_item_id)
      INTO v_eid_set
      FROM v3.rf_project_report_line rfprl
     WHERE rfprl.id = ANY(v_changed_lines);
    v_ei_path := v3._expense_item_path_set(v_eid_set);

    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form3_report_jsonb(p_report_id, NULL) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(v_changed_lines))
     ORDER BY v.sort_order;
  END;
END;
$function$
;

-- DROP FUNCTION v3.upd_form3_cell(int4, int4, text, jsonb);

CREATE OR REPLACE FUNCTION v3.upd_form3_cell(p_report_id integer, p_line_id integer, p_column text, p_value jsonb)
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE sql
AS $function$
  SELECT * FROM v3.upd_form3_cells(
    p_report_id,
    jsonb_build_array(jsonb_build_object(
      'line_id', p_line_id,
      'column',  p_column,
      'value',   p_value
    ))
  );
$function$
;

-- DROP FUNCTION v3.upd_form_cells(int4, varchar, jsonb, varchar, _text);

CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_change         JSONB;
  v_line_id        INT;
  v_column         TEXT;
  v_value          JSONB;
  v_changed_lines  INT[] := ARRAY[]::INT[];
  v_before         JSONB := '{}'::jsonb;   -- map sort_order(text) → data BEFORE
  v_lc_path        INT[];
  v_eid_set        INT[];
  v_ei_path        INT[];
BEGIN
  IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
    RAISE EXCEPTION 'p_changes must be a JSONB array';
  END IF;

  -- Проверка: form_id существует
  IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN
    RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
  END IF;

  -- Применяем все изменения в одной транзакции (вся функция = одна tx)
  FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
  LOOP
    v_line_id := (v_change->>'line_id')::INT;
    v_column  := v_change->>'column';
    v_value   := v_change->'value';   -- именно ->, чтобы NULL остался jsonb null

    IF v_line_id IS NULL OR v_column IS NULL THEN
      RAISE EXCEPTION 'change must have line_id and column: %', v_change;
    END IF;

    -- Sanity: для основной сетки — line_id принадлежит budget_form;
    -- для сателлитов — id принадлежит соответствующей таблице сателлита
    -- и через её line_id — данной budget_form;
    -- для AHR_LIMIT — id принадлежит limit_template (глобальный каталог,
    -- проверим что строка LEAF; per-form значения летят в form_limit).
    IF p_sheet = 'AHR_LIMIT' THEN
      IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt
                     WHERE lt.id = v_line_id AND lt.row_type = 'LEAF') THEN
        RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF', v_line_id;
      END IF;
    ELSIF p_sheet = 'OTCH9F' THEN
      IF NOT EXISTS (SELECT 1 FROM v3.fixed_asset_report
                     WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
        RAISE EXCEPTION 'fixed_asset_report.id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
      END IF;
    ELSIF p_sheet = 'AHR_RENT' THEN
      IF NOT EXISTS (SELECT 1 FROM v3.rent_detail rd
                     JOIN v3.budget_line bl ON bl.id = rd.line_id
                     WHERE rd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
        RAISE EXCEPTION 'rent_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
      END IF;
    ELSIF p_sheet = 'AHR_UTILITY' THEN
      IF NOT EXISTS (SELECT 1 FROM v3.utility_detail ud
                     JOIN v3.budget_line bl ON bl.id = ud.line_id
                     WHERE ud.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
        RAISE EXCEPTION 'utility_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
      END IF;
    ELSIF p_sheet = 'AHR_SECURITY' THEN
      IF NOT EXISTS (SELECT 1 FROM v3.security_detail sd
                     JOIN v3.budget_line bl ON bl.id = sd.line_id
                     WHERE sd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
        RAISE EXCEPTION 'security_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
      END IF;
    ELSE
      IF NOT EXISTS (SELECT 1 FROM v3.budget_line
                     WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
        RAISE EXCEPTION 'budget_line id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
      END IF;
    END IF;

    -- Сначала ТОЛЬКО регистрируем v_changed_lines, не применяем (для snapshot
    -- before нужно знать все затронутые до изменения).
    IF NOT (v_line_id = ANY(v_changed_lines)) THEN
      v_changed_lines := v_changed_lines || v_line_id;
    END IF;
  END LOOP;

  -- ── Вычисляем path-set для каждого типа листа (ОДИН раз, для before и after)
  IF p_sheet = 'AHR_LIMIT' THEN
    v_lc_path := v3._limit_template_path_set(v_changed_lines);
  ELSIF p_sheet NOT IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY','OTCH9F') THEN
    SELECT array_agg(DISTINCT bl.expense_item_id)
      INTO v_eid_set
      FROM v3.budget_line bl
     WHERE bl.id = ANY(v_changed_lines);
    v_ei_path := v3._expense_item_path_set(v_eid_set);
  END IF;

  -- ── Snapshot BEFORE (тот же фильтр что и для возврата) ────────────────
  IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
    SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb)
      INTO v_before
      FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
     WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(v_changed_lines);
  ELSIF p_sheet = 'AHR_LIMIT' THEN
    SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb)
      INTO v_before
      FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
     WHERE (v.data->>'id')::INT = ANY(v_lc_path);
  ELSIF p_sheet = 'OTCH9F' THEN
    SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb)
      INTO v_before
      FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
     WHERE EXISTS (
       SELECT 1 FROM v3.fixed_asset_report far
        WHERE far.id = ANY(v_changed_lines)
          AND far.expense_item_id = (v.data->>'expense_item_id')::INT
          AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'));
  ELSE
    SELECT COALESCE(jsonb_object_agg(v.sort_order::text, v.data), '{}'::jsonb)
      INTO v_before
      FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT'
            AND (v.data->>'line_id')::INT = ANY(v_changed_lines));
  END IF;

  -- ── Применяем изменения ─────────────────────────────────────────────
  FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
  LOOP
    PERFORM v3._apply_form_cell(
      (v_change->>'line_id')::INT,
      v_change->>'column',
      v_change->'value',
      p_sheet,
      p_form_id);
  END LOOP;

  -- ── Возврат: AFTER vs BEFORE diff. Только строки с непустым diff.
  -- Identifier-ключи (line_id / header.expense_item_id / id) гарантированно
  -- добавлены в diff, чтобы фронт мог match с своим state.
  IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
    RETURN QUERY
    WITH after_rows AS (
      SELECT v.row_type, v.depth, v.sort_order, v.data,
             v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff
        FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
       WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(v_changed_lines)
    )
    SELECT a.row_type, a.depth, a.sort_order,
           a.diff || jsonb_build_object('id', (a.data->>'id')::INT)
      FROM after_rows a
     WHERE a.diff <> '{}'::jsonb
     ORDER BY a.sort_order;

  ELSIF p_sheet = 'AHR_LIMIT' THEN
    RETURN QUERY
    WITH after_rows AS (
      SELECT v.row_type, v.depth, v.sort_order, v.data,
             v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff
        FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
       WHERE (v.data->>'id')::INT = ANY(v_lc_path)
    )
    SELECT a.row_type, a.depth, a.sort_order,
           a.diff || jsonb_build_object('id', (a.data->>'id')::INT)
      FROM after_rows a
     WHERE a.diff <> '{}'::jsonb
     ORDER BY a.sort_order;

  ELSIF p_sheet = 'OTCH9F' THEN
    RETURN QUERY
    WITH after_rows AS (
      SELECT v.row_type, v.depth, v.sort_order, v.data,
             v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff
        FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
       WHERE EXISTS (
         SELECT 1 FROM v3.fixed_asset_report far
          WHERE far.id = ANY(v_changed_lines)
            AND far.expense_item_id = (v.data->>'expense_item_id')::INT
            AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'))
    )
    SELECT a.row_type, a.depth, a.sort_order,
           a.diff || jsonb_build_object('id', (a.data->>'id')::INT)
      FROM after_rows a
     WHERE a.diff <> '{}'::jsonb
     ORDER BY a.sort_order;

  ELSE
    -- Основная сетка FORM_1/2/4
    RETURN QUERY
    WITH after_rows AS (
      SELECT v.row_type, v.depth, v.sort_order, v.data,
             v3._jsonb_diff(v_before->(v.sort_order::text), v.data) AS diff
        FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
       WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
              AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
          OR (v.row_type = 'INPUT'
              AND (v.data->>'line_id')::INT = ANY(v_changed_lines))
    )
    SELECT a.row_type, a.depth, a.sort_order,
           CASE
             WHEN a.row_type = 'INPUT' THEN
               a.diff || jsonb_build_object('line_id', (a.data->>'line_id')::INT)
             ELSE
               -- иерархия: добавить header.expense_item_id если diff его не содержит
               a.diff || jsonb_build_object('header',
                 COALESCE(a.diff->'header', '{}'::jsonb)
                 || jsonb_build_object('expense_item_id',
                      (a.data->'header'->>'expense_item_id')::INT))
           END
      FROM after_rows a
     WHERE a.diff <> '{}'::jsonb
     ORDER BY a.sort_order;
  END IF;
END;
$function$
;


-- DROP FUNCTION v3.upd_form_cells(int4, varchar, jsonb, int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_user_id integer, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_change   JSONB;
    v_column   TEXT;
    v_decision JSONB;
    v_msg      TEXT;
BEGIN
    IF p_user_id IS NULL THEN
        RAISE EXCEPTION 'role_not_allowed: <missing user_id>';
    END IF;

    IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN
        RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id;
    END IF;

    IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
        RAISE EXCEPTION 'p_changes must be a JSONB array';
    END IF;

    -- Проверяем КАЖДУЮ ячейку до начала записи. Падаем на первой запрещённой —
    -- транзакция целиком откатится, частичной записи не будет.
    FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
    LOOP
        v_column := v_change->>'column';
        IF v_column IS NULL THEN
            RAISE EXCEPTION 'change must have column: %', v_change;
        END IF;

        v_decision := v3.can_edit(p_form_id, p_sheet, v_column, p_user_id);

        IF NOT (v_decision->>'ok')::boolean THEN
            IF v_decision->>'code' = 'role_not_allowed' THEN
                RAISE EXCEPTION 'role_not_allowed: %', v_column;

            ELSIF v_decision->>'code' = 'org_not_assigned' THEN
                RAISE EXCEPTION 'org_not_assigned: %', v_column;

            ELSIF v_decision->>'code' = 'window_closed' THEN
                IF v_decision ? 'opens_at' THEN
                    v_msg := format('window_closed: %s (opens %s)',
                                    v_column, v_decision->>'opens_at');
                ELSIF v_decision->>'closes_at' IS NOT NULL THEN
                    v_msg := format('window_closed: %s (closed %s)',
                                    v_column, v_decision->>'closes_at');
                ELSE
                    v_msg := format('window_closed: %s (no phase)', v_column);
                END IF;
                RAISE EXCEPTION '%', v_msg;

            ELSE
                RAISE EXCEPTION '%: %', v_decision->>'code', v_column;
            END IF;
        END IF;
    END LOOP;

    -- Все ячейки разрешены — делегируем в v3.
    -- ROW_UPDATE намеренно НЕ логируется (см. logging_events.md): cell-edit'ы
    -- слишком частые. В audit_log пишем только структурные события
    -- (ROW_CREATE / ROW_DELETE) + бизнес-операции (CREATE_TASK и т.д.).
    RETURN QUERY
    SELECT * FROM v3.upd_form_cells(
        p_form_id, p_sheet, p_changes, p_direction, p_sections
    );
END;
$function$
;

-- DROP FUNCTION v3.upd_form_cell(int4, varchar, int4, text, jsonb, varchar, _text);

CREATE OR REPLACE FUNCTION v3.upd_form_cell(p_form_id integer, p_sheet character varying, p_budget_line_id integer, p_column text, p_value jsonb, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE sql
AS $function$
  SELECT * FROM v3.upd_form_cells(
    p_form_id,
    p_sheet,
    jsonb_build_array(jsonb_build_object(
      'line_id', p_budget_line_id,
      'column',  p_column,
      'value',   p_value
    )),
    p_direction,
    p_sections
  );
$function$
;

-- DROP FUNCTION v3.upd_form_cell(int4, varchar, int4, text, jsonb, int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.upd_form_cell(p_form_id integer, p_sheet character varying, p_budget_line_id integer, p_column text, p_value jsonb, p_user_id integer, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE sql
AS $function$
    SELECT * FROM v3.upd_form_cells(
        p_form_id,
        p_sheet,
        jsonb_build_array(jsonb_build_object(
            'line_id', p_budget_line_id,
            'column',  p_column,
            'value',   p_value
        )),
        p_user_id,
        p_direction,
        p_sections
    );
$function$
;

-- DROP FUNCTION v3.upd_form_phase(int4, varchar, varchar, varchar, _text, timestamptz, timestamptz);

CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp with time zone DEFAULT NULL::timestamp with time zone, p_closes_at timestamp with time zone DEFAULT NULL::timestamp with time zone)
 RETURNS v3.form_phase
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old v3.form_phase;
    v_new v3.form_phase;
    v_is_extend BOOLEAN := FALSE;
    v_has_non_extend_change BOOLEAN := FALSE;
    v_has_window_change BOOLEAN := FALSE;
    v_changes JSONB := '{}'::jsonb;
    v_org_unit_id INT;
    v_org_unit_title VARCHAR;
    v_form_type_code VARCHAR;
    v_year INT;
    v_task_name TEXT;
    v_phase_id TEXT;
BEGIN
    SELECT * INTO v_old
      FROM v3.form_phase
     WHERE budget_form_id = p_budget_form_id
       AND sheet = p_sheet
       AND phase_code = p_phase_code;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)',
          p_budget_form_id, p_sheet, p_phase_code;
    END IF;

    UPDATE v3.form_phase
       SET role        = COALESCE(p_role, role),
           column_keys = COALESCE(p_column_keys, column_keys),
           opens_at    = COALESCE(p_opens_at, opens_at),
           closes_at   = COALESCE(p_closes_at, closes_at)
     WHERE budget_form_id = p_budget_form_id
       AND sheet = p_sheet
       AND phase_code = p_phase_code
     RETURNING * INTO v_new;

    SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
      INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
      FROM v3.budget_form bf
      LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
     WHERE bf.id = p_budget_form_id;
    v_task_name := trim(concat(
        COALESCE(v_form_type_code, ''),
        CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
        CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
    ));
    v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);

    v_is_extend := v_new.closes_at > v_old.closes_at;
    v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
        OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
        OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
    v_has_window_change := v_has_non_extend_change
        OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);

    IF v_old.role IS DISTINCT FROM v_new.role THEN
        v_changes := v_changes || jsonb_build_object(
            'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
        );
    END IF;
    IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
        v_changes := v_changes || jsonb_build_object(
            'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
        );
    END IF;
    IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
        v_changes := v_changes || jsonb_build_object(
            'opens_at', jsonb_build_object(
                'before', v_old.opens_at,
                'after', v_new.opens_at
            )
        );
    END IF;
    IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
        v_changes := v_changes || jsonb_build_object(
            'closes_at', jsonb_build_object(
                'before', v_old.closes_at,
                'after', v_new.closes_at
            )
        );
    END IF;

    IF v_is_extend THEN
        PERFORM v3.log_event(
            'ACCESS_EXTEND', 'ACCESS',
            jsonb_build_object(
                'phase_id', v_phase_id,
                'phase_name', p_phase_code,
                'task_id', p_budget_form_id,
                'task_name', NULLIF(v_task_name, ''),
                'form_id', p_budget_form_id,
                'budget_form_id', p_budget_form_id,
                'org_unit_id', v_org_unit_id,
                'org_unit_name', v_org_unit_title,
                'sheet', p_sheet,
                'phase_code', p_phase_code,
                'closes_at_before', v_old.closes_at,
                'closes_at_after', v_new.closes_at
            ),
            p_budget_form_id
        );
    END IF;

    IF v_has_window_change THEN
        PERFORM v3.log_event(
            'ACCESS_WINDOW_CHANGE', 'ACCESS',
            jsonb_build_object(
                'entity_type', 'form_phase',
                'entity_id', p_budget_form_id,
                'action', 'update',
                'phase_id', v_phase_id,
                'phase_name', p_phase_code,
                'task_id', p_budget_form_id,
                'task_name', NULLIF(v_task_name, ''),
                'form_id', p_budget_form_id,
                'budget_form_id', p_budget_form_id,
                'org_unit_id', v_org_unit_id,
                'org_unit_name', v_org_unit_title,
                'sheet', p_sheet,
                'phase_code', p_phase_code,
                'changes', v_changes
            ),
            p_budget_form_id, null, v_org_unit_id
        );
    END IF;

    RETURN v_new;
END;
$function$
;

-- DROP FUNCTION v3.upd_project(int4, text, jsonb);

CREATE OR REPLACE FUNCTION v3.upd_project(p_project_id integer, p_column text, p_value jsonb)
 RETURNS jsonb
 LANGUAGE plpgsql
AS $function$
DECLARE
  v_target_col  TEXT;
  v_target_type TEXT;
  v_sql         TEXT;
  v_out         JSONB;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
    RAISE EXCEPTION 'project id=% не существует', p_project_id;
  END IF;

  -- Editable атрибуты (CHECK-ы из migrate_form3_v1.sql применятся при UPDATE).
  -- Не editable: level (структура), parent_id (иерархия).
  CASE p_column
    WHEN 'name'           THEN v_target_col := 'name';           v_target_type := 'TEXT';
    WHEN 'project_type'   THEN v_target_col := 'project_type';   v_target_type := 'TEXT';
    WHEN 'vsp_format'     THEN v_target_col := 'vsp_format';     v_target_type := 'TEXT';
    WHEN 'placement_type' THEN v_target_col := 'placement_type'; v_target_type := 'TEXT';
    WHEN 'object_address' THEN v_target_col := 'object_address'; v_target_type := 'TEXT';
    WHEN 'staff_count'    THEN v_target_col := 'staff_count';    v_target_type := 'INTEGER';
    WHEN 'total_area'     THEN v_target_col := 'total_area';     v_target_type := 'NUMERIC';
    WHEN 'org_unit_id'    THEN v_target_col := 'org_unit_id';    v_target_type := 'INTEGER';
    WHEN 'level','parent_id' THEN
      RAISE EXCEPTION 'structural_field: % (правится отдельно через ADD project / структурный API)', p_column;
    WHEN 'id' THEN
      RAISE EXCEPTION 'key_field: id (RO)';
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
  END CASE;

  -- FK санити (если применимо)
  IF v_target_col = 'org_unit_id' AND p_value <> 'null'::jsonb THEN
    IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = (p_value #>> '{}')::INT) THEN
      RAISE EXCEPTION 'org_unit id=% не существует', (p_value #>> '{}');
    END IF;
  END IF;

  v_sql := format(
    'UPDATE v3.project SET %1$I = ($2 #>> ''{}'')::%2$s WHERE id = $1',
    v_target_col, v_target_type
  );
  EXECUTE v_sql USING p_project_id, p_value;

  -- Возврат — весь header проекта
  SELECT jsonb_build_object(
           'id',             p.id,
           'name',           p.name,
           'level',          p.level,
           'parent_id',      p.parent_id,
           'org_unit_id',    p.org_unit_id,
           'project_type',   p.project_type,
           'vsp_format',     p.vsp_format,
           'placement_type', p.placement_type,
           'object_address', p.object_address,
           'staff_count',    p.staff_count,
           'total_area',     p.total_area
         )
    INTO v_out
    FROM v3.project p WHERE p.id = p_project_id;
  RETURN v_out;
END;
$function$
;

-- DROP FUNCTION v3.upd_user(int4, varchar, varchar, varchar, varchar, int4, bool);

CREATE OR REPLACE FUNCTION v3.upd_user(p_user_id integer, p_email character varying DEFAULT NULL::character varying, p_username character varying DEFAULT NULL::character varying, p_hashed_password character varying DEFAULT NULL::character varying, p_full_name character varying DEFAULT NULL::character varying, p_role_id integer DEFAULT NULL::integer, p_is_active boolean DEFAULT NULL::boolean)
 RETURNS v3.app_user
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old_role_id INT;
    v_new v3.app_user;
BEGIN
    SELECT role_id INTO v_old_role_id
      FROM v3.app_user
     WHERE id = p_user_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'app_user #% не существует', p_user_id;
    END IF;

    UPDATE v3.app_user
       SET email           = COALESCE(p_email, email),
           username        = COALESCE(p_username, username),
           hashed_password = COALESCE(p_hashed_password, hashed_password),
           full_name       = COALESCE(p_full_name, full_name),
           role_id         = COALESCE(p_role_id, role_id),
           is_active       = COALESCE(p_is_active, is_active),
           updated_at      = now()
     WHERE id = p_user_id
     RETURNING * INTO v_new;

    IF v_old_role_id IS DISTINCT FROM v_new.role_id THEN
        PERFORM v3.log_event(
            'USER_ROLE_CHANGE', 'USER',
            jsonb_build_object(
                'user_id', p_user_id,
                'entity_id', p_user_id,
                'email', v_new.email,
                'full_name', v_new.full_name,
                'role_id_before', v_old_role_id,
                'role_id_after', v_new.role_id
            )
        );
    END IF;

    RETURN v_new;
END;
$function$
;

-- DROP FUNCTION v3.upd_vsp(int4, int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, date);

CREATE OR REPLACE FUNCTION v3.upd_vsp(p_vsp_id integer, p_branch_id integer DEFAULT NULL::integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT NULL::boolean, p_is_deleted boolean DEFAULT NULL::boolean, p_system_code character varying DEFAULT NULL::character varying, p_updated_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
DECLARE
    v_old RECORD;
    v_changes JSONB := '{}'::jsonb;

    v_branch_name_before VARCHAR;
    v_branch_name_after  VARCHAR;

    v_actor_id INT;
    v_actor_email VARCHAR;
    v_actor_full_name VARCHAR;
BEGIN
    SELECT v.branch_id, v.reg_number, v.address, v.format, v.opened_at,
           v.placement_type, v.staff_count, v.total_area, v.closed_at,
           v.is_active, v.is_deleted, v.system_code,
           v.vsp_type, v.notes, v.rent_contract_num, v.rent_end_date
      INTO v_old
      FROM v3.vsp v
     WHERE v.id = p_vsp_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
    END IF;

    SELECT ou.title
      INTO v_branch_name_before
      FROM v3.org_unit ou
     WHERE ou.id = v_old.branch_id;

    v_branch_name_after := v_branch_name_before;

    v_actor_id := COALESCE(p_updated_by, v3.current_user_id());
    IF v_actor_id IS NOT NULL THEN
        SELECT u.email, u.full_name
          INTO v_actor_email, v_actor_full_name
          FROM v3.app_user u
         WHERE u.id = v_actor_id;
    END IF;

    IF p_branch_id IS NOT NULL AND p_branch_id IS DISTINCT FROM v_old.branch_id THEN
        SELECT ou.title INTO v_branch_name_after
        FROM v3.org_unit ou
        WHERE ou.id = p_branch_id;

        UPDATE v3.vsp SET branch_id = p_branch_id WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object(
            'branch_id', jsonb_build_object(
                'before', v3._diff_val(v_old.branch_id),
                'after',  v3._diff_val(p_branch_id)
            ),
            'branch_name', jsonb_build_object(
                'before', v3._diff_val(v_branch_name_before),
                'after',  v3._diff_val(v_branch_name_after)
            )
        );
    END IF;

    IF p_reg_number IS NOT NULL AND p_reg_number IS DISTINCT FROM v_old.reg_number THEN
        UPDATE v3.vsp SET reg_number = p_reg_number WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('reg_number',
            jsonb_build_object('before', v3._diff_val(v_old.reg_number),
                               'after',  v3._diff_val(p_reg_number)));
    END IF;

    IF p_address IS NOT NULL AND p_address IS DISTINCT FROM v_old.address THEN
        UPDATE v3.vsp SET address = p_address WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('address',
            jsonb_build_object('before', v3._diff_val(v_old.address),
                               'after',  v3._diff_val(p_address)));
    END IF;

    IF p_format IS NOT NULL AND p_format IS DISTINCT FROM v_old.format THEN
        UPDATE v3.vsp SET format = p_format WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('format',
            jsonb_build_object('before', v3._diff_val(v_old.format),
                               'after',  v3._diff_val(p_format)));
    END IF;

    IF p_opened_at IS NOT NULL AND p_opened_at IS DISTINCT FROM v_old.opened_at THEN
        UPDATE v3.vsp SET opened_at = p_opened_at WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('opened_at',
            jsonb_build_object('before', v3._diff_val(v_old.opened_at),
                               'after',  v3._diff_val(p_opened_at)));
    END IF;

    IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN
        UPDATE v3.vsp SET placement_type = p_placement_type WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('placement_type',
            jsonb_build_object('before', v3._diff_val(v_old.placement_type),
                               'after',  v3._diff_val(p_placement_type)));
    END IF;

    IF p_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN
        UPDATE v3.vsp SET staff_count = p_staff_count WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('staff_count',
            jsonb_build_object('before', v3._diff_val(v_old.staff_count),
                               'after',  v3._diff_val(p_staff_count)));
    END IF;

    IF p_total_area IS NOT NULL AND p_total_area IS DISTINCT FROM v_old.total_area THEN
        UPDATE v3.vsp SET total_area = p_total_area WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('total_area',
            jsonb_build_object('before', v3._diff_val(v_old.total_area),
                               'after',  v3._diff_val(p_total_area)));
    END IF;

    IF p_closed_at IS NOT NULL AND p_closed_at IS DISTINCT FROM v_old.closed_at THEN
        UPDATE v3.vsp SET closed_at = p_closed_at WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('closed_at',
            jsonb_build_object('before', v3._diff_val(v_old.closed_at),
                               'after',  v3._diff_val(p_closed_at)));
    END IF;

    IF p_is_active IS NOT NULL AND p_is_active IS DISTINCT FROM v_old.is_active THEN
        UPDATE v3.vsp SET is_active = p_is_active WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('is_active',
            jsonb_build_object('before', v3._diff_val(v_old.is_active),
                               'after',  v3._diff_val(p_is_active)));
    END IF;

    IF p_is_deleted IS NOT NULL AND p_is_deleted IS DISTINCT FROM v_old.is_deleted THEN
        UPDATE v3.vsp SET is_deleted = p_is_deleted WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('is_deleted',
            jsonb_build_object('before', v3._diff_val(v_old.is_deleted),
                               'after',  v3._diff_val(p_is_deleted)));
    END IF;

    IF p_system_code IS NOT NULL AND p_system_code IS DISTINCT FROM v_old.system_code THEN
        UPDATE v3.vsp SET system_code = p_system_code WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('system_code',
            jsonb_build_object('before', v3._diff_val(v_old.system_code),
                               'after',  v3._diff_val(p_system_code)));
    END IF;

    IF p_vsp_type IS NOT NULL AND p_vsp_type IS DISTINCT FROM v_old.vsp_type THEN
        UPDATE v3.vsp SET vsp_type = p_vsp_type WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('vsp_type',
            jsonb_build_object('before', v3._diff_val(v_old.vsp_type),
                               'after',  v3._diff_val(p_vsp_type)));
    END IF;

    IF p_notes IS NOT NULL AND p_notes IS DISTINCT FROM v_old.notes THEN
        UPDATE v3.vsp SET notes = p_notes WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('notes',
            jsonb_build_object('before', v3._diff_val(v_old.notes),
                               'after',  v3._diff_val(p_notes)));
    END IF;

    IF p_rent_contract_num IS NOT NULL AND p_rent_contract_num IS DISTINCT FROM v_old.rent_contract_num THEN
        UPDATE v3.vsp SET rent_contract_num = p_rent_contract_num WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('rent_contract_num',
            jsonb_build_object('before', v3._diff_val(v_old.rent_contract_num),
                               'after',  v3._diff_val(p_rent_contract_num)));
    END IF;

    IF p_rent_end_date IS NOT NULL AND p_rent_end_date IS DISTINCT FROM v_old.rent_end_date THEN
        UPDATE v3.vsp SET rent_end_date = p_rent_end_date WHERE id = p_vsp_id;
        v_changes := v_changes || jsonb_build_object('rent_end_date',
            jsonb_build_object('before', v3._diff_val(v_old.rent_end_date),
                               'after',  v3._diff_val(p_rent_end_date)));
    END IF;

    IF v_changes <> '{}'::jsonb THEN
        UPDATE v3.vsp
           SET updated_at = now(),
               updated_by = p_updated_by
         WHERE id = p_vsp_id;

        PERFORM v3.log_event(
            'VSP_UPDATE', 'VSP',
            jsonb_build_object(
                'vsp_id', p_vsp_id,
                'entity_id', p_vsp_id,
                'branch_id', COALESCE(p_branch_id, v_old.branch_id),
                'branch_name', COALESCE(v_branch_name_after, v_branch_name_before),
                'changes', v_changes,
                'user_email', v_actor_email,
                'user_full_name', v_actor_full_name
            ), null, null, COALESCE(p_branch_id, v_old.branch_id)
        );
    END IF;
END;
$function$
;

-- DROP FUNCTION v3.user_in_form_org(int4, int4);

CREATE OR REPLACE FUNCTION v3.user_in_form_org(p_user_id integer, p_form_id integer)
 RETURNS boolean
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    v_org INT;
BEGIN
    SELECT org_unit_id INTO v_org
      FROM v3.budget_form
     WHERE id = p_form_id;

    IF v_org IS NULL THEN
        RETURN TRUE;     -- форма не привязана к org_unit — пропускаем фильтр
    END IF;

    RETURN EXISTS (
        SELECT 1
          FROM v3.user_org
         WHERE user_id = p_user_id
           AND org_unit_id = v_org
    );
END;
$function$
;

-- DROP FUNCTION v3.user_role_code(int4);

CREATE OR REPLACE FUNCTION v3.user_role_code(p_user_id integer)
 RETURNS character varying
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    v_code VARCHAR;
BEGIN
    SELECT r.code
      INTO v_code
      FROM v3.app_user u
      JOIN v3.role     r ON r.id = u.role_id
     WHERE u.id = p_user_id
       AND u.is_active;
    RETURN v_code;       -- NULL если юзер не найден или неактивен
END;
$function$
;

-- DROP FUNCTION v3.v_form1_sheet_sections(int4, varchar, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form1_sheet_sections(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_b_year integer, col_c_section character varying, col_d_item_id character varying, col_e_num_group character varying, col_f_name character varying, col_g_internal_order character varying, col_h_plan_q1 numeric, col_i_plan_q2 numeric, col_j_plan_q3 numeric, col_k_plan_q4 numeric, col_l_plan_year numeric, col_m_plan_comment character varying, col_n_cs_total numeric, col_o_cs_counterparty character varying, col_p_cs_deadline character varying, col_q_cs_comment character varying, col_r_cs_future_y1 numeric, col_s_cs_future_y2 numeric, col_t_cs_other_ssp numeric, col_v_al_order character varying, col_w_al_property character varying, col_y_seq_q1 numeric, col_z_seq_q2 numeric, col_aa_seq_q3 numeric, col_ab_seq_q4 numeric, col_ac_seq_year numeric, col_ad_seq_justification character varying, col_af_res_q1 numeric, col_ag_res_q2 numeric, col_ah_res_q3 numeric, col_ai_res_q4 numeric, col_aj_res_year numeric, col_ak_res_justification character varying, col_am_approved_q1 numeric, col_an_approved_q2 numeric, col_ao_approved_q3 numeric, col_ap_approved_q4 numeric, col_aq_approved_year numeric, col_as_ca_approved numeric, col_at_ca_protocol character varying, col_au_ca_note character varying, col_aw_ckk_ceiling numeric, col_ax_ckk_exp_q1 numeric, col_ay_ckk_exp_q2 numeric, col_az_ckk_exp_q3 numeric, col_ba_ckk_exp_q4 numeric, col_bb_ckk_rf_sched character varying, col_bc_ckk_deadline character varying, col_bd_ckk_proc_plan character varying, col_be_ckk_proc_meth character varying, col_bf_ckk_comment character varying, col_bh_cd_counterparty character varying, col_bi_cd_reference character varying, col_bj_cd_addenda character varying, col_bk_cd_subject character varying, col_bl_cd_currency character varying, col_bm_cd_ceiling numeric, col_bn_cd_exp_q1 numeric, col_bo_cd_exp_q2 numeric, col_bp_cd_exp_q3 numeric, col_bq_cd_exp_q4 numeric, col_br_cd_rf_sched character varying, col_bs_cd_vat character varying, col_bt_cd_rate numeric, col_bu_cd_amount_fx numeric, col_bv_cd_deadline character varying, col_bw_cd_scheme character varying, col_bx_cd_act character varying, col_by_cd_comment character varying, col_ca_q1_adj_current numeric, col_cb_q1_adj_ssp numeric, col_cc_q1_adj_rf numeric, col_cd_q1_adj_reserve numeric, col_ce_q1_adj_comment character varying, col_cf_q1_corrected_plan numeric, col_ch_q1_pay_date date, col_ci_q1_pay_amount numeric, col_cj_q1_pay_ho numeric, col_ck_q1_pay_rf numeric, col_cl_q1_pay_comment character varying, col_cm_q1_pay_act character varying, col_co_q1_booking numeric, col_cp_q1_actual_m1 numeric, col_cq_q1_actual_m2 numeric, col_cr_q1_actual_m3 numeric, col_cs_q1_actual_quarter numeric, col_ct_q1_rem_booking numeric, col_cu_q1_rem_actual numeric, col_cw_q1_trans_q2 numeric, col_cx_q1_trans_q3 numeric, col_cy_q1_trans_q4 numeric, col_cz_q1_economy numeric, col_da_q1_total numeric, col_dc_q2_rev_eco numeric, col_dd_q2_rev_item numeric, col_de_q2_rev_inc numeric, col_df_q2_rev_seq numeric, col_dg_q2_rev_comment character varying, col_dh_q2_new_plan numeric, col_dj_q2_adj_current numeric, col_dk_q2_adj_ssp numeric, col_dl_q2_adj_rf numeric, col_dm_q2_adj_reserve numeric, col_dn_q2_adj_comment character varying, col_do_q2_corrected_plan numeric, col_dq_q2_pay_date date, col_dr_q2_pay_amount numeric, col_ds_q2_pay_ho numeric, col_dt_q2_pay_rf numeric, col_du_q2_pay_comment character varying, col_dv_q2_pay_act character varying, col_dx_q2_booking numeric, col_dy_q2_actual_m1 numeric, col_dz_q2_actual_m2 numeric, col_ea_q2_actual_m3 numeric, col_eb_q2_actual_quarter numeric, col_ec_q2_rem_booking numeric, col_ed_q2_rem_actual numeric, col_ef_q2_trans_q3 numeric, col_eg_q2_trans_q4 numeric, col_eh_q2_economy numeric, col_ei_q2_total numeric, col_ek_q3_rev_eco numeric, col_el_q3_rev_item numeric, col_em_q3_rev_inc numeric, col_en_q3_rev_seq numeric, col_eo_q3_rev_comment character varying, col_ep_q3_new_plan numeric, col_er_q3_adj_current numeric, col_es_q3_adj_ssp numeric, col_et_q3_adj_rf numeric, col_eu_q3_adj_reserve numeric, col_ev_q3_adj_comment character varying, col_ew_q3_corrected_plan numeric, col_ey_q3_pay_date date, col_ez_q3_pay_amount numeric, col_fa_q3_pay_ho numeric, col_fb_q3_pay_rf numeric, col_fc_q3_pay_comment character varying, col_fd_q3_pay_act character varying, col_ff_q3_booking numeric, col_fg_q3_actual_m1 numeric, col_fh_q3_actual_m2 numeric, col_fi_q3_actual_m3 numeric, col_fj_q3_actual_quarter numeric, col_fk_q3_rem_booking numeric, col_fl_q3_rem_actual numeric, col_fn_q3_trans_q4 numeric, col_fo_q3_economy numeric, col_fp_q3_total numeric, col_fr_q4_rev_eco numeric, col_fs_q4_rev_item numeric, col_ft_q4_rev_inc numeric, col_fu_q4_rev_seq numeric, col_fv_q4_rev_comment character varying, col_fw_q4_new_plan numeric, col_fy_q4_adj_current numeric, col_fz_q4_adj_ssp numeric, col_ga_q4_adj_rf numeric, col_gb_q4_adj_reserve numeric, col_gc_q4_adj_comment character varying, col_gd_q4_corrected_plan numeric, col_gf_q4_pay_date date, col_gg_q4_pay_amount numeric, col_gh_q4_pay_ho numeric, col_gi_q4_pay_rf numeric, col_gj_q4_pay_comment character varying, col_gk_q4_pay_act character varying, col_gm_q4_booking numeric, col_gn_q4_actual_m1 numeric, col_go_q4_actual_m2 numeric, col_gp_q4_actual_m3 numeric, col_gq_q4_actual_quarter numeric, col_gr_q4_actual_spod numeric, col_gs_q4_rem_booking numeric, col_gt_q4_rem_actual numeric, col_gv_q4_economy numeric, col_gw_q4_total numeric, col_gz_fact_year numeric, col_hb_pay_year numeric, _sort_path integer[])
 LANGUAGE plpgsql
 STABLE
 SET search_path TO 'v3', 'pg_catalog'
AS $function$
#variable_conflict use_column
DECLARE
  v_year  INT;
  s_plan BOOL; s_cs BOOL; s_al BOOL; s_seq BOOL; s_res BOOL;
  s_appr BOOL; s_ca BOOL; s_ckk BOOL; s_cd BOOL;
  s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL;
  s_any_q BOOL;
  s_need_ap BOOL;   -- plan/seq/res нужны: approved И/ИЛИ cp1..cp4
  s_need_bk BOOL;   -- ckk/cd нужны: booking bk1..bk4 (в Q-секциях)
BEGIN
  SELECT bf.year INTO v_year FROM budget_form bf WHERE bf.id = p_form_id;

  s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections);
  s_cs   := p_sections IS NULL OR 'contract_summary' = ANY(p_sections);
  s_al   := p_sections IS NULL OR 'allocation' = ANY(p_sections);
  s_seq  := p_sections IS NULL OR 'sequestration' = ANY(p_sections);
  s_res  := p_sections IS NULL OR 'reserve' = ANY(p_sections);
  s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections);
  s_ca   := p_sections IS NULL OR 'collegial' = ANY(p_sections);
  s_ckk  := p_sections IS NULL OR 'ckk' = ANY(p_sections);
  s_cd   := p_sections IS NULL OR 'contract_detail' = ANY(p_sections);
  s_q1   := p_sections IS NULL OR 'q1' = ANY(p_sections);
  s_q2   := p_sections IS NULL OR 'q2' = ANY(p_sections);
  s_q3   := p_sections IS NULL OR 'q3' = ANY(p_sections);
  s_q4   := p_sections IS NULL OR 'q4' = ANY(p_sections);
  s_tot  := p_sections IS NULL OR 'totals' = ANY(p_sections);
  s_any_q := s_q1 OR s_q2 OR s_q3 OR s_q4 OR s_tot;
  s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4;
  s_need_bk := s_q1 OR s_q2 OR s_q3 OR s_q4;

  RETURN QUERY
  WITH
  -- ═══ 1. Дерево expense_item из materialized view ══════════════════════
  -- JOIN с expense_item_form_type фильтрует узлы по form_type текущей формы
  tw AS (
    SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name,
           t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids
    FROM mv_expense_item_tree t
    JOIN expense_item_form_type eft ON eft.expense_item_id = t.id
    JOIN budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code
    WHERE t.sheet = p_sheet
  ),

  -- ═══ 2. Все INPUT строки формы ══════════════════════════════════════════
  -- p_direction: фильтр по budget_line.direction. NULL = без фильтра.
  -- bl.direction IS NULL обходит фильтр (для листов без direction, напр. Операц).
  page AS (
    SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.internal_order AS io,
           bl.name AS bname
    FROM budget_line bl
    JOIN expense_item ei ON ei.id = bl.expense_item_id
    WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
      AND (p_direction IS NULL OR bl.direction IS NULL OR bl.direction = p_direction)
  ),

  -- ═══ 3. Satellite JOINs (условные) ════════════════════════════════════
  jp AS (SELECT p.* FROM plan p JOIN page pg ON p.line_id = pg.lid WHERE s_plan),
  jcs AS (SELECT cs.* FROM contract_summary cs JOIN page pg ON cs.line_id = pg.lid WHERE s_cs),
  jal AS (SELECT al.* FROM allocation al JOIN page pg ON al.line_id = pg.lid WHERE s_al),
  jsd AS (SELECT sd.* FROM sequestration sd JOIN page pg ON sd.line_id = pg.lid WHERE s_seq AND sd.actor='DFIP'),
  jr AS (SELECT r.* FROM reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_res),
  jca AS (SELECT ca.* FROM collegial_approval ca JOIN page pg ON ca.line_id = pg.lid WHERE s_ca),
  jck AS (SELECT ck.* FROM ckk ck JOIN page pg ON ck.line_id = pg.lid WHERE s_ckk),
  jcd AS (SELECT cd.* FROM contract_detail cd JOIN page pg ON cd.line_id = pg.lid WHERE s_cd),
  -- Direct quarter joins (no pivot — WHERE quarter=N)
  jq1 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q1 AND blq.quarter = 1),
  jq2 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q2 AND blq.quarter = 2),
  jq3 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q3 AND blq.quarter = 3),
  jq4 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE (s_q4 OR s_tot) AND blq.quarter = 4),
  -- For totals: need all 4 quarters to sum actual across year — but only when s_tot and not all q loaded
  jtot AS (
    SELECT blq.line_id AS tlid,
      SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq1,
      SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq2,
      SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq3,
      SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS tot_aq4,
      SUM(CASE WHEN blq.quarter=1 THEN blq.payment_amount END) AS tot_pa1,
      SUM(CASE WHEN blq.quarter=2 THEN blq.payment_amount END) AS tot_pa2,
      SUM(CASE WHEN blq.quarter=3 THEN blq.payment_amount END) AS tot_pa3,
      SUM(CASE WHEN blq.quarter=4 THEN blq.payment_amount END) AS tot_pa4
    FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid
    WHERE s_tot
    GROUP BY blq.line_id
  ),

  -- ═══ 4. INPUT строки: enriched с computed ═════════════════════════════
  -- Промежуточные вычисления для computed-полей
  input_base AS (
    SELECT
      pg.lid, pg.eid, pg.io, pg.bname,
      -- section_code = parent's item_id (hierarchy code like 1.01.1.)
      t.parent_item_id AS sc,
      -- item_id = R-code for depth=3, else hierarchy code
      t.item_id AS ic,
      t.num_group_id AS ng, t.name AS ename,
      -- raw satellite values (for computed)
      COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2,
      COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4,
      p.comment AS pcmt,
      COALESCE(sd.adj_q1,0) AS sq1, COALESCE(sd.adj_q2,0) AS sq2,
      COALESCE(sd.adj_q3,0) AS sq3, COALESCE(sd.adj_q4,0) AS sq4,
      sd.justification AS sjust,
      COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2,
      COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4,
      r.justification AS rjust,
      cs.*, al.internal_order AS al_io, al.property_object AS al_po,
      ca.approved_amount AS ca_amt, ca.protocol_reference AS ca_prot, ca.note AS ca_note,
      ck.ceiling_amount AS ckk_ceil,
      COALESCE(ck.expenses_q1,0) AS ckk1, COALESCE(ck.expenses_q2,0) AS ckk2,
      COALESCE(ck.expenses_q3,0) AS ckk3, COALESCE(ck.expenses_q4,0) AS ckk4,
      ck.rf_schedule AS ckk_rf, ck.delivery_deadline AS ckk_dl,
      ck.procurement_plan AS ckk_pp, ck.procurement_method AS ckk_pm, ck.comment AS ckk_cmt,
      cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.addenda AS cd_add,
      cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil,
      COALESCE(cd.expenses_q1,0) AS cdq1, COALESCE(cd.expenses_q2,0) AS cdq2,
      COALESCE(cd.expenses_q3,0) AS cdq3, COALESCE(cd.expenses_q4,0) AS cdq4,
      cd.rf_schedule AS cd_rf, cd.vat_rate AS cd_vat, cd.exchange_rate AS cd_rate,
      cd.amount_foreign AS cd_fx, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch,
      cd.act AS cd_act, cd.comment AS cd_cmt2,
      -- Q1 direct
      q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_rf AS q1_ar, q1.adj_reserve AS q1_arv,
      q1.adj_comment AS q1_acmt, q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa,
      q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf,
      q1.payment_comment AS q1_pcmt, q1.payment_act AS q1_pact,
      q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3,
      q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4,
      -- Q2 direct
      q2.plan_revision_eco_change AS q2_reco, q2.plan_revision_item_adj AS q2_ritm,
      q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq,
      q2.plan_revision_comment AS q2_rcmt,
      q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_rf AS q2_ar, q2.adj_reserve AS q2_arv,
      q2.adj_comment AS q2_acmt, q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa,
      q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf,
      q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact,
      q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3,
      q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4,
      -- Q3 direct
      q3.plan_revision_eco_change AS q3_reco, q3.plan_revision_item_adj AS q3_ritm,
      q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq,
      q3.plan_revision_comment AS q3_rcmt,
      q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_rf AS q3_ar, q3.adj_reserve AS q3_arv,
      q3.adj_comment AS q3_acmt, q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa,
      q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf,
      q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact,
      q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3,
      q3.transfer_to_q4 AS q3_tq4,
      -- Q4 direct
      q4.plan_revision_eco_change AS q4_reco, q4.plan_revision_item_adj AS q4_ritm,
      q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq,
      q4.plan_revision_comment AS q4_rcmt,
      q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_rf AS q4_ar, q4.adj_reserve AS q4_arv,
      q4.adj_comment AS q4_acmt, q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa,
      q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf,
      q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact,
      q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3,
      q4.actual_spod AS q4_spod,
      -- totals
      tot.tot_aq1, tot.tot_aq2, tot.tot_aq3, tot.tot_aq4,
      tot.tot_pa1, tot.tot_pa2, tot.tot_pa3, tot.tot_pa4,
      -- booking per quarter
      CASE WHEN COALESCE(cd.expenses_q1,0)>0 THEN cd.expenses_q1 ELSE ck.expenses_q1 END AS bk1,
      CASE WHEN COALESCE(cd.expenses_q2,0)>0 THEN cd.expenses_q2 ELSE ck.expenses_q2 END AS bk2,
      CASE WHEN COALESCE(cd.expenses_q3,0)>0 THEN cd.expenses_q3 ELSE ck.expenses_q3 END AS bk3,
      CASE WHEN COALESCE(cd.expenses_q4,0)>0 THEN cd.expenses_q4 ELSE ck.expenses_q4 END AS bk4,
      -- approved per quarter
      COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1,
      COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2,
      COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3,
      COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4,
      -- tree path for ordering
      t.path AS tree_path
    FROM page pg
    JOIN tw t ON t.id = pg.eid
    LEFT JOIN jp p   ON p.line_id  = pg.lid
    LEFT JOIN jcs cs ON cs.line_id = pg.lid
    LEFT JOIN jal al ON al.line_id = pg.lid
    LEFT JOIN jsd sd ON sd.line_id = pg.lid
    LEFT JOIN jr r   ON r.line_id  = pg.lid
    LEFT JOIN jca ca ON ca.line_id = pg.lid
    LEFT JOIN jck ck ON ck.line_id = pg.lid
    LEFT JOIN jcd cd ON cd.line_id = pg.lid
    LEFT JOIN jq1 q1 ON q1.line_id = pg.lid
    LEFT JOIN jq2 q2 ON q2.line_id = pg.lid
    LEFT JOIN jq3 q3 ON q3.line_id = pg.lid
    LEFT JOIN jq4 q4 ON q4.line_id = pg.lid
    LEFT JOIN jtot tot ON tot.tlid  = pg.lid
  ),
  -- Q1-Q4 computed fields
  enriched AS (
    SELECT b.*,
      -- Q1 corrected_plan
      b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_ar,0)+COALESCE(b.q1_arv,0) AS cp1,
      -- Q1 actual_quarter
      COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1,
      -- Q2 new_plan
      b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_reco,0)+COALESCE(b.q2_ritm,0)+COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) AS np2,
      -- Q2 actual_quarter
      COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2,
      -- Q3 new_plan
      b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_reco,0)+COALESCE(b.q3_ritm,0)+COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) AS np3,
      -- Q3 actual_quarter
      COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3,
      -- Q4 new_plan
      b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_reco,0)+COALESCE(b.q4_ritm,0)+COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) AS np4,
      -- Q4 actual_quarter (без СПОД)
      COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0) AS aq4_no_spod,
      -- Q4 actual with СПОД
      COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4
    FROM input_base b
  ),
  -- Final computed: corrected_plan Qn = new_plan + adj, rem/economy/total
  final_input AS (
    SELECT e.*,
      -- Q2 corrected_plan
      e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_ar,0)+COALESCE(e.q2_arv,0) AS cp2,
      -- Q3 corrected_plan
      e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_ar,0)+COALESCE(e.q3_arv,0) AS cp3,
      -- Q4 corrected_plan
      e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_ar,0)+COALESCE(e.q4_arv,0) AS cp4
    FROM enriched e
  ),

  -- ═══ 5. Агрегаты для иерархии ═════════════════════════════════════════
  agg AS (
    SELECT bl.expense_item_id AS eid,
      SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4,
      SUM(sd.adj_q1) AS ss1, SUM(sd.adj_q2) AS ss2, SUM(sd.adj_q3) AS ss3, SUM(sd.adj_q4) AS ss4,
      SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4,
      SUM(ca.approved_amount) AS sca,
      SUM(ck.ceiling_amount) AS sckc,
      SUM(ck.expenses_q1) AS sck1, SUM(ck.expenses_q2) AS sck2, SUM(ck.expenses_q3) AS sck3, SUM(ck.expenses_q4) AS sck4,
      SUM(cd.ceiling_amount) AS scdc,
      SUM(cd.expenses_q1) AS scd1, SUM(cd.expenses_q2) AS scd2, SUM(cd.expenses_q3) AS scd3, SUM(cd.expenses_q4) AS scd4,
      SUM(cs.total_amount) AS scst, SUM(cs.future_payments_y1) AS sfy1, SUM(cs.future_payments_y2) AS sfy2,
      SUM(cs.other_ssp_amount) AS sossp
    FROM budget_line bl
    JOIN expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN plan p               ON p.line_id  = bl.id AND s_need_ap
    LEFT JOIN contract_summary cs  ON cs.line_id = bl.id AND s_cs
    LEFT JOIN sequestration sd     ON sd.line_id = bl.id AND sd.actor = 'DFIP' AND s_need_ap
    LEFT JOIN reserve r            ON r.line_id  = bl.id AND s_need_ap
    LEFT JOIN collegial_approval ca ON ca.line_id = bl.id AND s_ca
    LEFT JOIN ckk ck               ON ck.line_id = bl.id AND (s_ckk OR s_need_bk)
    LEFT JOIN contract_detail cd   ON cd.line_id = bl.id AND (s_cd  OR s_need_bk)
    WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
      AND (s_need_ap OR s_cs OR s_ca OR s_ckk OR s_cd OR s_need_bk)
    GROUP BY bl.expense_item_id
  ),
  -- Per-quarter hierarchy aggregates (no pivot — direct WHERE quarter=N)
  aq1 AS (
    SELECT bl.expense_item_id AS eid,
      SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv,
      SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf,
      SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3,
      SUM(blq.transfer_to_q2) AS tq2, SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4
    FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 1
    WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q1 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
    GROUP BY bl.expense_item_id
  ),
  aq2 AS (
    SELECT bl.expense_item_id AS eid,
      SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm,
      SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq,
      SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv,
      SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf,
      SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3,
      SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4
    FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 2
    WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q2 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
    GROUP BY bl.expense_item_id
  ),
  aq3 AS (
    SELECT bl.expense_item_id AS eid,
      SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm,
      SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq,
      SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv,
      SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf,
      SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3,
      SUM(blq.transfer_to_q4) AS tq4
    FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 3
    WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q3 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
    GROUP BY bl.expense_item_id
  ),
  aq4 AS (
    SELECT bl.expense_item_id AS eid,
      SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm,
      SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq,
      SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv,
      SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf,
      SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3,
      SUM(blq.actual_spod) AS spod
    FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 4
    WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_q4 OR s_tot) AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
    GROUP BY bl.expense_item_id
  ),
  atot AS (
    SELECT bl.expense_item_id AS eid,
      SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq1,
      SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq2,
      SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq3,
      SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS fq4,
      SUM(blq.payment_amount) AS pay
    FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id
    WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_tot AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
    GROUP BY bl.expense_item_id
  ),
  -- ═══ Pre-computed per-node aggregates (one pass over tw × agg) ═════════
  tw_agg AS (
    SELECT tw.id,
      SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4,
      SUM(a.ss1) AS ss1, SUM(a.ss2) AS ss2, SUM(a.ss3) AS ss3, SUM(a.ss4) AS ss4,
      SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4,
      SUM(a.sca) AS sca,
      SUM(a.sckc) AS sckc, SUM(a.sck1) AS sck1, SUM(a.sck2) AS sck2, SUM(a.sck3) AS sck3, SUM(a.sck4) AS sck4,
      SUM(a.scdc) AS scdc, SUM(a.scd1) AS scd1, SUM(a.scd2) AS scd2, SUM(a.scd3) AS scd3, SUM(a.scd4) AS scd4,
      SUM(a.scst) AS scst, SUM(a.sfy1) AS sfy1, SUM(a.sfy2) AS sfy2, SUM(a.sossp) AS sossp,
      SUM(CASE WHEN COALESCE(a.scd1,0)>0 THEN a.scd1 ELSE a.sck1 END) AS bk1,
      SUM(CASE WHEN COALESCE(a.scd2,0)>0 THEN a.scd2 ELSE a.sck2 END) AS bk2,
      SUM(CASE WHEN COALESCE(a.scd3,0)>0 THEN a.scd3 ELSE a.sck3 END) AS bk3,
      SUM(CASE WHEN COALESCE(a.scd4,0)>0 THEN a.scd4 ELSE a.sck4 END) AS bk4
    FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids)
    GROUP BY tw.id
  ),
  tw_aq1 AS (
    SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv,
      SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf,
      SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
      SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4
    FROM tw LEFT JOIN aq1 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id
  ),
  tw_aq2 AS (
    SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq,
      SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv,
      SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf,
      SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
      SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4
    FROM tw LEFT JOIN aq2 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id
  ),
  tw_aq3 AS (
    SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq,
      SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv,
      SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf,
      SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
      SUM(b.tq4) AS tq4
    FROM tw LEFT JOIN aq3 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id
  ),
  tw_aq4 AS (
    SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq,
      SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv,
      SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf,
      SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
      SUM(b.spod) AS spod
    FROM tw LEFT JOIN aq4 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id
  ),
  tw_atot AS (
    SELECT tw.id, SUM(b.fq1) AS fq1, SUM(b.fq2) AS fq2, SUM(b.fq3) AS fq3, SUM(b.fq4) AS fq4,
      SUM(b.pay) AS pay
    FROM tw LEFT JOIN atot b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id
  )

  -- ═══ ЧАСТЬ A: INPUT строки ════════════════════════════════════════════
  SELECT * FROM (
  SELECT
    'INPUT'::VARCHAR, 3, f.lid,
    v_year, f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.io,
    CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END,
    CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END,
    CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END,
    CASE WHEN s_cs THEN f.total_amount END, CASE WHEN s_cs THEN f.counterparty END,
    CASE WHEN s_cs THEN f.deadline END, CASE WHEN s_cs THEN f.comment END,
    CASE WHEN s_cs THEN f.future_payments_y1 END, CASE WHEN s_cs THEN f.future_payments_y2 END,
    CASE WHEN s_cs THEN f.other_ssp_amount END,
    CASE WHEN s_al THEN f.al_io END, CASE WHEN s_al THEN f.al_po END,
    CASE WHEN s_seq THEN f.sq1 END, CASE WHEN s_seq THEN f.sq2 END,
    CASE WHEN s_seq THEN f.sq3 END, CASE WHEN s_seq THEN f.sq4 END,
    CASE WHEN s_seq THEN f.sq1+f.sq2+f.sq3+f.sq4 END, CASE WHEN s_seq THEN f.sjust END,
    CASE WHEN s_res THEN f.rq1 END, CASE WHEN s_res THEN f.rq2 END,
    CASE WHEN s_res THEN f.rq3 END, CASE WHEN s_res THEN f.rq4 END,
    CASE WHEN s_res THEN f.rq1+f.rq2+f.rq3+f.rq4 END, CASE WHEN s_res THEN f.rjust END,
    CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END,
    CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END,
    CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END,
    CASE WHEN s_ca THEN f.ca_amt END, CASE WHEN s_ca THEN f.ca_prot END, CASE WHEN s_ca THEN f.ca_note END,
    CASE WHEN s_ckk THEN f.ckk_ceil END, CASE WHEN s_ckk THEN f.ckk1 END, CASE WHEN s_ckk THEN f.ckk2 END,
    CASE WHEN s_ckk THEN f.ckk3 END, CASE WHEN s_ckk THEN f.ckk4 END,
    CASE WHEN s_ckk THEN f.ckk_rf END, CASE WHEN s_ckk THEN f.ckk_dl END,
    CASE WHEN s_ckk THEN f.ckk_pp END, CASE WHEN s_ckk THEN f.ckk_pm END, CASE WHEN s_ckk THEN f.ckk_cmt END,
    CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, CASE WHEN s_cd THEN f.cd_add END,
    CASE WHEN s_cd THEN f.cd_subj END, CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END,
    CASE WHEN s_cd THEN f.cdq1 END, CASE WHEN s_cd THEN f.cdq2 END,
    CASE WHEN s_cd THEN f.cdq3 END, CASE WHEN s_cd THEN f.cdq4 END,
    CASE WHEN s_cd THEN f.cd_rf END, CASE WHEN s_cd THEN f.cd_vat END,
    CASE WHEN s_cd THEN f.cd_rate END, CASE WHEN s_cd THEN f.cd_fx END,
    CASE WHEN s_cd THEN f.cd_dl END, CASE WHEN s_cd THEN f.cd_sch END,
    CASE WHEN s_cd THEN f.cd_act END, CASE WHEN s_cd THEN f.cd_cmt2 END,
    -- Q1
    CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END,
    CASE WHEN s_q1 THEN f.q1_ar END, CASE WHEN s_q1 THEN f.q1_arv END,
    CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END,
    CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END,
    CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END,
    CASE WHEN s_q1 THEN f.q1_pcmt END, CASE WHEN s_q1 THEN f.q1_pact END,
    CASE WHEN s_q1 THEN COALESCE(f.bk1,0) END,
    CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END,
    CASE WHEN s_q1 THEN f.aq1 END,
    CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.bk1,0) END,
    CASE WHEN s_q1 THEN f.cp1 - f.aq1 END,
    CASE WHEN s_q1 THEN f.q1_tq2 END, CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END,
    CASE WHEN s_q1 THEN f.cp1 - f.aq1 - COALESCE(f.q1_tq2,0) - COALESCE(f.q1_tq3,0) - COALESCE(f.q1_tq4,0) END,
    CASE WHEN s_q1 THEN f.cp1 - f.aq1 END,
    -- Q2
    CASE WHEN s_q2 THEN f.q2_reco END, CASE WHEN s_q2 THEN f.q2_ritm END,
    CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END,
    CASE WHEN s_q2 THEN f.q2_rcmt END, CASE WHEN s_q2 THEN f.np2 END,
    CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END,
    CASE WHEN s_q2 THEN f.q2_ar END, CASE WHEN s_q2 THEN f.q2_arv END,
    CASE WHEN s_q2 THEN f.q2_acmt END, CASE WHEN s_q2 THEN f.cp2 END,
    CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END,
    CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END,
    CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END,
    CASE WHEN s_q2 THEN COALESCE(f.bk2,0) END,
    CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END,
    CASE WHEN s_q2 THEN f.aq2 END,
    CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.bk2,0) END,
    CASE WHEN s_q2 THEN f.cp2 - f.aq2 END,
    CASE WHEN s_q2 THEN f.q2_tq3 END, CASE WHEN s_q2 THEN f.q2_tq4 END,
    CASE WHEN s_q2 THEN f.cp2 - f.aq2 - COALESCE(f.q2_tq3,0) - COALESCE(f.q2_tq4,0) END,
    CASE WHEN s_q2 THEN f.cp2 - f.aq2 END,
    -- Q3
    CASE WHEN s_q3 THEN f.q3_reco END, CASE WHEN s_q3 THEN f.q3_ritm END,
    CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END,
    CASE WHEN s_q3 THEN f.q3_rcmt END, CASE WHEN s_q3 THEN f.np3 END,
    CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END,
    CASE WHEN s_q3 THEN f.q3_ar END, CASE WHEN s_q3 THEN f.q3_arv END,
    CASE WHEN s_q3 THEN f.q3_acmt END, CASE WHEN s_q3 THEN f.cp3 END,
    CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END,
    CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END,
    CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END,
    CASE WHEN s_q3 THEN COALESCE(f.bk3,0) END,
    CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END,
    CASE WHEN s_q3 THEN f.aq3 END,
    CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.bk3,0) END,
    CASE WHEN s_q3 THEN f.cp3 - f.aq3 END,
    CASE WHEN s_q3 THEN f.q3_tq4 END,
    CASE WHEN s_q3 THEN f.cp3 - f.aq3 - COALESCE(f.q3_tq4,0) END,
    CASE WHEN s_q3 THEN f.cp3 - f.aq3 END,
    -- Q4
    CASE WHEN s_q4 THEN f.q4_reco END, CASE WHEN s_q4 THEN f.q4_ritm END,
    CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END,
    CASE WHEN s_q4 THEN f.q4_rcmt END, CASE WHEN s_q4 THEN f.np4 END,
    CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END,
    CASE WHEN s_q4 THEN f.q4_ar END, CASE WHEN s_q4 THEN f.q4_arv END,
    CASE WHEN s_q4 THEN f.q4_acmt END, CASE WHEN s_q4 THEN f.cp4 END,
    CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END,
    CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END,
    CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END,
    CASE WHEN s_q4 THEN COALESCE(f.bk4,0) END,
    CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END,
    CASE WHEN s_q4 THEN f.aq4_no_spod END,
    CASE WHEN s_q4 THEN f.q4_spod END,
    CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.bk4,0) END,
    CASE WHEN s_q4 THEN f.cp4 - f.aq4 END,
    CASE WHEN s_q4 THEN f.cp4 - f.aq4 END,
    CASE WHEN s_q4 THEN f.cp4 - f.aq4 END,
    -- totals
    CASE WHEN s_tot THEN COALESCE(f.tot_aq1,0)+COALESCE(f.tot_aq2,0)+COALESCE(f.tot_aq3,0)+COALESCE(f.tot_aq4,0) END,
    CASE WHEN s_tot THEN COALESCE(f.tot_pa1,0)+COALESCE(f.tot_pa2,0)+COALESCE(f.tot_pa3,0)+COALESCE(f.tot_pa4,0) END,
    f.tree_path || ARRAY[f.lid] AS _sort
  FROM final_input f

  UNION ALL

  -- ═══ ЧАСТЬ B: Строки иерархии ═════════════════════════════════════════
  SELECT
    CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
    t.depth, NULL::INT,
    v_year,
    -- col_C = hierarchy code for depth 0-2, parent's code for depth 3 (SUB_ITEM)
    CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
    -- col_D = NULL for hierarchy, R-code for SUB_ITEM
    CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL::VARCHAR END,
    t.num_group_id, t.name, NULL::VARCHAR,
    -- plan
    CASE WHEN s_plan THEN ta.sp1 END,
    CASE WHEN s_plan THEN ta.sp2 END,
    CASE WHEN s_plan THEN ta.sp3 END,
    CASE WHEN s_plan THEN ta.sp4 END,
    CASE WHEN s_plan THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0)) END,
    NULL::VARCHAR,
    -- contract_summary
    CASE WHEN s_cs THEN ta.scst END,
    NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
    CASE WHEN s_cs THEN ta.sfy1 END,
    CASE WHEN s_cs THEN ta.sfy2 END,
    CASE WHEN s_cs THEN ta.sossp END,
    NULL::VARCHAR, NULL::VARCHAR,
    -- sequestration
    CASE WHEN s_seq THEN ta.ss1 END,
    CASE WHEN s_seq THEN ta.ss2 END,
    CASE WHEN s_seq THEN ta.ss3 END,
    CASE WHEN s_seq THEN ta.ss4 END,
    CASE WHEN s_seq THEN (COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0)) END,
    NULL::VARCHAR,
    -- reserve
    CASE WHEN s_res THEN ta.sr1 END,
    CASE WHEN s_res THEN ta.sr2 END,
    CASE WHEN s_res THEN ta.sr3 END,
    CASE WHEN s_res THEN ta.sr4 END,
    CASE WHEN s_res THEN (COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END,
    NULL::VARCHAR,
    -- approved
    CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) END,
    CASE WHEN s_appr THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) END,
    CASE WHEN s_appr THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) END,
    CASE WHEN s_appr THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) END,
    CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0)
      +COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0)
      +COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END,
    -- collegial
    CASE WHEN s_ca THEN ta.sca END,
    NULL::VARCHAR, NULL::VARCHAR,
    -- ckk
    CASE WHEN s_ckk THEN ta.sckc END,
    CASE WHEN s_ckk THEN ta.sck1 END,
    CASE WHEN s_ckk THEN ta.sck2 END,
    CASE WHEN s_ckk THEN ta.sck3 END,
    CASE WHEN s_ckk THEN ta.sck4 END,
    NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
    -- contract_detail
    NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
    CASE WHEN s_cd THEN ta.scdc END,
    CASE WHEN s_cd THEN ta.scd1 END,
    CASE WHEN s_cd THEN ta.scd2 END,
    CASE WHEN s_cd THEN ta.scd3 END,
    CASE WHEN s_cd THEN ta.scd4 END,
    NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
    -- Q1 hierarchy (from aq1)
    CASE WHEN s_q1 THEN tb1.ac END,
    CASE WHEN s_q1 THEN tb1.as2 END,
    CASE WHEN s_q1 THEN tb1.ar END,
    CASE WHEN s_q1 THEN tb1.arv END,
    NULL::VARCHAR,
    -- corrected_plan Q1 = approved_q1 + SUM(adj_*)
    CASE WHEN s_q1 THEN
      (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0))
      + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)), 0)
    END,
    NULL::DATE,
    CASE WHEN s_q1 THEN tb1.pa END,
    CASE WHEN s_q1 THEN tb1.pho END,
    CASE WHEN s_q1 THEN tb1.prf END,
    NULL::VARCHAR, NULL::VARCHAR,
    -- booking Q1 = SUM(CASE cd>0 THEN cd ELSE ckk)
    CASE WHEN s_q1 THEN ta.bk1 END,
    CASE WHEN s_q1 THEN tb1.m1 END,
    CASE WHEN s_q1 THEN tb1.m2 END,
    CASE WHEN s_q1 THEN tb1.m3 END,
    CASE WHEN s_q1 THEN (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) END,
    -- rem_booking Q1 = corrected_plan - booking (ckk or cd)
    CASE WHEN s_q1 THEN
      (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0))
      + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0)
      - COALESCE(ta.bk1,0)
    END,
    -- rem_actual Q1 = corrected_plan - actual_quarter
    CASE WHEN s_q1 THEN
      (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0))
      + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0)
      - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0)
    END,
    CASE WHEN s_q1 THEN tb1.tq2 END,
    CASE WHEN s_q1 THEN tb1.tq3 END,
    CASE WHEN s_q1 THEN tb1.tq4 END,
    -- economy Q1 = rem_actual - transfers
    CASE WHEN s_q1 THEN
      (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0))
      + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0)
      - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0)
      - COALESCE((COALESCE(tb1.tq2,0)+COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)),0)
    END,
    -- total Q1 = rem_actual
    CASE WHEN s_q1 THEN
      (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0))
      + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0)
      - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0)
    END,
    -- Q2 hierarchy (from aq2)
    CASE WHEN s_q2 THEN tb2.reco END,
    CASE WHEN s_q2 THEN tb2.ritm END,
    CASE WHEN s_q2 THEN tb2.rinc END,
    CASE WHEN s_q2 THEN tb2.rseq END,
    NULL::VARCHAR,
    -- new_plan Q2 = approved_q2 + transfer_q1→q2 + revisions
    CASE WHEN s_q2 THEN
      (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0))
      + COALESCE(tb1.tq2, 0)
      + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)), 0)
    END,
    CASE WHEN s_q2 THEN tb2.ac END,
    CASE WHEN s_q2 THEN tb2.as2 END,
    CASE WHEN s_q2 THEN tb2.ar END,
    CASE WHEN s_q2 THEN tb2.arv END,
    NULL::VARCHAR,
    -- corrected_plan Q2 = new_plan_q2 + adj_*
    CASE WHEN s_q2 THEN
      (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0))
      + COALESCE(tb1.tq2, 0)
      + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)), 0)
    END,
    NULL::DATE,
    CASE WHEN s_q2 THEN tb2.pa END,
    CASE WHEN s_q2 THEN tb2.pho END,
    CASE WHEN s_q2 THEN tb2.prf END,
    NULL::VARCHAR, NULL::VARCHAR,
    CASE WHEN s_q2 THEN ta.bk2 END,
    CASE WHEN s_q2 THEN tb2.m1 END,
    CASE WHEN s_q2 THEN tb2.m2 END,
    CASE WHEN s_q2 THEN tb2.m3 END,
    CASE WHEN s_q2 THEN (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) END,
    -- rem_booking Q2
    CASE WHEN s_q2 THEN
      (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0))
      + COALESCE(tb1.tq2,0)
      + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0)
      - COALESCE(ta.bk2,0)
    END,
    -- rem_actual Q2
    CASE WHEN s_q2 THEN
      (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0))
      + COALESCE(tb1.tq2,0)
      + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0)
      - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0)
    END,
    CASE WHEN s_q2 THEN tb2.tq3 END,
    CASE WHEN s_q2 THEN tb2.tq4 END,
    -- economy Q2
    CASE WHEN s_q2 THEN
      (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0))
      + COALESCE(tb1.tq2,0)
      + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0)
      - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0)
      - COALESCE((COALESCE(tb2.tq3,0)+COALESCE(tb2.tq4,0)),0)
    END,
    -- total Q2 = rem_actual
    CASE WHEN s_q2 THEN
      (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0))
      + COALESCE(tb1.tq2,0)
      + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0)
      - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0)
    END,
    -- Q3 hierarchy (from aq3)
    CASE WHEN s_q3 THEN tb3.reco END,
    CASE WHEN s_q3 THEN tb3.ritm END,
    CASE WHEN s_q3 THEN tb3.rinc END,
    CASE WHEN s_q3 THEN tb3.rseq END,
    NULL::VARCHAR,
    -- new_plan Q3 = approved_q3 + transfer_q1→q3 + transfer_q2→q3 + revisions
    CASE WHEN s_q3 THEN
      (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0))
      + COALESCE(tb1.tq3, 0)
      + COALESCE(tb2.tq3, 0)
      + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)), 0)
    END,
    CASE WHEN s_q3 THEN tb3.ac END,
    CASE WHEN s_q3 THEN tb3.as2 END,
    CASE WHEN s_q3 THEN tb3.ar END,
    CASE WHEN s_q3 THEN tb3.arv END,
    NULL::VARCHAR,
    -- corrected_plan Q3 = new_plan + adj_*
    CASE WHEN s_q3 THEN
      (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0))
      + COALESCE(tb1.tq3, 0)
      + COALESCE(tb2.tq3, 0)
      + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)), 0)
    END,
    NULL::DATE,
    CASE WHEN s_q3 THEN tb3.pa END,
    CASE WHEN s_q3 THEN tb3.pho END,
    CASE WHEN s_q3 THEN tb3.prf END,
    NULL::VARCHAR, NULL::VARCHAR,
    CASE WHEN s_q3 THEN ta.bk3 END,
    CASE WHEN s_q3 THEN tb3.m1 END,
    CASE WHEN s_q3 THEN tb3.m2 END,
    CASE WHEN s_q3 THEN tb3.m3 END,
    CASE WHEN s_q3 THEN (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) END,
    -- rem_booking Q3
    CASE WHEN s_q3 THEN
      (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0))
      + COALESCE(tb1.tq3,0)
      + COALESCE(tb2.tq3,0)
      + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0)
      - COALESCE(ta.bk3,0)
    END,
    -- rem_actual Q3
    CASE WHEN s_q3 THEN
      (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0))
      + COALESCE(tb1.tq3,0)
      + COALESCE(tb2.tq3,0)
      + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0)
      - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0)
    END,
    CASE WHEN s_q3 THEN tb3.tq4 END,
    -- economy Q3
    CASE WHEN s_q3 THEN
      (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0))
      + COALESCE(tb1.tq3,0)
      + COALESCE(tb2.tq3,0)
      + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0)
      - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0)
      - COALESCE(COALESCE(tb3.tq4,0),0)
    END,
    -- total Q3 = rem_actual
    CASE WHEN s_q3 THEN
      (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0))
      + COALESCE(tb1.tq3,0)
      + COALESCE(tb2.tq3,0)
      + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0)
      - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0)
    END,
    -- Q4 hierarchy (from aq4)
    CASE WHEN s_q4 THEN tb4.reco END,
    CASE WHEN s_q4 THEN tb4.ritm END,
    CASE WHEN s_q4 THEN tb4.rinc END,
    CASE WHEN s_q4 THEN tb4.rseq END,
    NULL::VARCHAR,
    -- new_plan Q4 = approved_q4 + transfers_q1→q4 + q2→q4 + q3→q4 + revisions
    CASE WHEN s_q4 THEN
      (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0))
      + COALESCE(tb1.tq4, 0)
      + COALESCE(tb2.tq4, 0)
      + COALESCE(tb3.tq4, 0)
      + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)), 0)
    END,
    CASE WHEN s_q4 THEN tb4.ac END,
    CASE WHEN s_q4 THEN tb4.as2 END,
    CASE WHEN s_q4 THEN tb4.ar END,
    CASE WHEN s_q4 THEN tb4.arv END,
    NULL::VARCHAR,
    -- corrected_plan Q4 = new_plan + adj_*
    CASE WHEN s_q4 THEN
      (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0))
      + COALESCE(tb1.tq4, 0)
      + COALESCE(tb2.tq4, 0)
      + COALESCE(tb3.tq4, 0)
      + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)), 0)
    END,
    NULL::DATE,
    CASE WHEN s_q4 THEN tb4.pa END,
    CASE WHEN s_q4 THEN tb4.pho END,
    CASE WHEN s_q4 THEN tb4.prf END,
    NULL::VARCHAR, NULL::VARCHAR,
    CASE WHEN s_q4 THEN ta.bk4 END,
    CASE WHEN s_q4 THEN tb4.m1 END,
    CASE WHEN s_q4 THEN tb4.m2 END,
    CASE WHEN s_q4 THEN tb4.m3 END,
    CASE WHEN s_q4 THEN (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)) END,
    CASE WHEN s_q4 THEN tb4.spod END,
    -- rem_booking Q4
    CASE WHEN s_q4 THEN
      (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0))
      + COALESCE(tb1.tq4,0)
      + COALESCE(tb2.tq4,0)
      + COALESCE(tb3.tq4,0)
      + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0)
      - COALESCE(ta.bk4,0)
    END,
    -- rem_actual Q4 (with spod)
    CASE WHEN s_q4 THEN
      (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0))
      + COALESCE(tb1.tq4,0)
      + COALESCE(tb2.tq4,0)
      + COALESCE(tb3.tq4,0)
      + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0)
      - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0)
    END,
    -- economy Q4 = rem_actual (no transfers from Q4)
    CASE WHEN s_q4 THEN
      (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0))
      + COALESCE(tb1.tq4,0)
      + COALESCE(tb2.tq4,0)
      + COALESCE(tb3.tq4,0)
      + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0)
      - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0)
    END,
    -- total Q4 = rem_actual
    CASE WHEN s_q4 THEN
      (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0))
      + COALESCE(tb1.tq4,0)
      + COALESCE(tb2.tq4,0)
      + COALESCE(tb3.tq4,0)
      + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0)
      - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0)
    END,
    -- totals hierarchy (from atot)
    CASE WHEN s_tot THEN (COALESCE(tt.fq1,0)+COALESCE(tt.fq2,0)+COALESCE(tt.fq3,0)+COALESCE(tt.fq4,0)) END,
    CASE WHEN s_tot THEN tt.pay END,
    t.path AS _sort

  FROM tw t
  LEFT JOIN tw_agg ta ON ta.id = t.id
  LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id
  LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id
  LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id
  LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id
  LEFT JOIN tw_atot tt ON tt.id = t.id
  ) sub
  ORDER BY sub._sort;

END;
$function$
;

-- DROP FUNCTION v3.v_form1_sheet_jsonb(int4, varchar, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form1_sheet_jsonb(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, contract_summary jsonb, allocation_data jsonb, sequestration_data jsonb, reserve_data jsonb, approved_data jsonb, collegial_data jsonb, ckk_data jsonb, contract_detail jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[])
 LANGUAGE sql
 STABLE
 SET search_path TO 'v3', 'pg_catalog'
AS $function$
  WITH flags AS (
    SELECT
      (p_sections IS NULL OR 'plan'             = ANY(p_sections)) AS s_plan,
      (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs,
      (p_sections IS NULL OR 'allocation'       = ANY(p_sections)) AS s_al,
      (p_sections IS NULL OR 'sequestration'    = ANY(p_sections)) AS s_seq,
      (p_sections IS NULL OR 'reserve'          = ANY(p_sections)) AS s_res,
      (p_sections IS NULL OR 'approved'         = ANY(p_sections)) AS s_appr,
      (p_sections IS NULL OR 'collegial'        = ANY(p_sections)) AS s_ca,
      (p_sections IS NULL OR 'ckk'              = ANY(p_sections)) AS s_ckk,
      (p_sections IS NULL OR 'contract_detail'  = ANY(p_sections)) AS s_cd,
      (p_sections IS NULL OR 'q1'               = ANY(p_sections)) AS s_q1,
      (p_sections IS NULL OR 'q2'               = ANY(p_sections)) AS s_q2,
      (p_sections IS NULL OR 'q3'               = ANY(p_sections)) AS s_q3,
      (p_sections IS NULL OR 'q4'               = ANY(p_sections)) AS s_q4,
      (p_sections IS NULL OR 'totals'           = ANY(p_sections)) AS s_tot
  )
  SELECT
    s.row_type, s.depth, s.line_id,

    -- header (всегда). expense_item_id извлекается из _sort_path:
    -- для INPUT путь = {...ei_id, line_id} → предпоследний элемент
    -- для иерархии путь = {...ei_id}        → последний элемент
    jsonb_build_object(
      'year',            s.col_B_year,
      'section',         s.col_C_section,
      'item_id',         s.col_D_item_id,
      'num_group',       s.col_E_num_group,
      'name',            s.col_F_name,
      'internal_order',  s.col_G_internal_order,
      'expense_item_id', CASE WHEN s.row_type = 'INPUT'
                           THEN s._sort_path[array_upper(s._sort_path,1)-1]
                           ELSE s._sort_path[array_upper(s._sort_path,1)] END
    ) AS header,

    CASE WHEN f.s_plan THEN jsonb_build_object(
      'q1',      s.col_H_plan_q1,
      'q2',      s.col_I_plan_q2,
      'q3',      s.col_J_plan_q3,
      'q4',      s.col_K_plan_q4,
      'year',    s.col_L_plan_year,
      'comment', s.col_M_plan_comment
    ) END AS plan_data,

    CASE WHEN f.s_cs THEN jsonb_build_object(
      'total',        s.col_N_cs_total,
      'counterparty', s.col_O_cs_counterparty,
      'deadline',     s.col_P_cs_deadline,
      'comment',      s.col_Q_cs_comment,
      'future_y1',    s.col_R_cs_future_y1,
      'future_y2',    s.col_S_cs_future_y2,
      'other_ssp',    s.col_T_cs_other_ssp
    ) END AS contract_summary,

    CASE WHEN f.s_al THEN jsonb_build_object(
      'order',    s.col_V_al_order,
      'property', s.col_W_al_property
    ) END AS allocation_data,

    CASE WHEN f.s_seq THEN jsonb_build_object(
      'q1',            s.col_Y_seq_q1,
      'q2',            s.col_Z_seq_q2,
      'q3',            s.col_AA_seq_q3,
      'q4',            s.col_AB_seq_q4,
      'year',          s.col_AC_seq_year,
      'justification', s.col_AD_seq_justification
    ) END AS sequestration_data,

    CASE WHEN f.s_res THEN jsonb_build_object(
      'q1',            s.col_AF_res_q1,
      'q2',            s.col_AG_res_q2,
      'q3',            s.col_AH_res_q3,
      'q4',            s.col_AI_res_q4,
      'year',          s.col_AJ_res_year,
      'justification', s.col_AK_res_justification
    ) END AS reserve_data,

    CASE WHEN f.s_appr THEN jsonb_build_object(
      'q1',   s.col_AM_approved_q1,
      'q2',   s.col_AN_approved_q2,
      'q3',   s.col_AO_approved_q3,
      'q4',   s.col_AP_approved_q4,
      'year', s.col_AQ_approved_year
    ) END AS approved_data,

    CASE WHEN f.s_ca THEN jsonb_build_object(
      'approved', s.col_AS_ca_approved,
      'protocol', s.col_AT_ca_protocol,
      'note',     s.col_AU_ca_note
    ) END AS collegial_data,

    CASE WHEN f.s_ckk THEN jsonb_build_object(
      'ceiling',     s.col_AW_ckk_ceiling,
      'q1',          s.col_AX_ckk_exp_q1,
      'q2',          s.col_AY_ckk_exp_q2,
      'q3',          s.col_AZ_ckk_exp_q3,
      'q4',          s.col_BA_ckk_exp_q4,
      'rf_schedule', s.col_BB_ckk_rf_sched,
      'deadline',    s.col_BC_ckk_deadline,
      'proc_plan',   s.col_BD_ckk_proc_plan,
      'proc_method', s.col_BE_ckk_proc_meth,
      'comment',     s.col_BF_ckk_comment
    ) END AS ckk_data,

    CASE WHEN f.s_cd THEN jsonb_build_object(
      'counterparty',   s.col_BH_cd_counterparty,
      'reference',      s.col_BI_cd_reference,
      'addenda',        s.col_BJ_cd_addenda,
      'subject',        s.col_BK_cd_subject,
      'currency',       s.col_BL_cd_currency,
      'ceiling',        s.col_BM_cd_ceiling,
      'q1',             s.col_BN_cd_exp_q1,
      'q2',             s.col_BO_cd_exp_q2,
      'q3',             s.col_BP_cd_exp_q3,
      'q4',             s.col_BQ_cd_exp_q4,
      'rf_schedule',    s.col_BR_cd_rf_sched,
      'vat_rate',       s.col_BS_cd_vat,
      'exchange_rate',  s.col_BT_cd_rate,
      'amount_foreign', s.col_BU_cd_amount_fx,
      'deadline',       s.col_BV_cd_deadline,
      'payment_scheme', s.col_BW_cd_scheme,
      'act',            s.col_BX_cd_act,
      'comment',        s.col_BY_cd_comment
    ) END AS contract_detail,

    CASE WHEN f.s_q1 THEN jsonb_build_object(
      'adj_current',    s.col_CA_q1_adj_current,
      'adj_ssp',        s.col_CB_q1_adj_ssp,
      'adj_rf',         s.col_CC_q1_adj_rf,
      'adj_reserve',    s.col_CD_q1_adj_reserve,
      'adj_comment',    s.col_CE_q1_adj_comment,
      'corrected_plan', s.col_CF_q1_corrected_plan,
      'pay_date',       s.col_CH_q1_pay_date,
      'pay_amount',     s.col_CI_q1_pay_amount,
      'pay_ho',         s.col_CJ_q1_pay_ho,
      'pay_rf',         s.col_CK_q1_pay_rf,
      'pay_comment',    s.col_CL_q1_pay_comment,
      'pay_act',        s.col_CM_q1_pay_act,
      'booking',        s.col_CO_q1_booking,
      'actual_m1',      s.col_CP_q1_actual_m1,
      'actual_m2',      s.col_CQ_q1_actual_m2,
      'actual_m3',      s.col_CR_q1_actual_m3,
      'actual_quarter', s.col_CS_q1_actual_quarter,
      'rem_booking',    s.col_CT_q1_rem_booking,
      'rem_actual',     s.col_CU_q1_rem_actual,
      'transfer_q2',    s.col_CW_q1_trans_q2,
      'transfer_q3',    s.col_CX_q1_trans_q3,
      'transfer_q4',    s.col_CY_q1_trans_q4,
      'economy',        s.col_CZ_q1_economy,
      'total',          s.col_DA_q1_total
    ) END AS q1_data,

    CASE WHEN f.s_q2 THEN jsonb_build_object(
      'rev_eco',        s.col_DC_q2_rev_eco,
      'rev_item',       s.col_DD_q2_rev_item,
      'rev_inc',        s.col_DE_q2_rev_inc,
      'rev_seq',        s.col_DF_q2_rev_seq,
      'rev_comment',    s.col_DG_q2_rev_comment,
      'new_plan',       s.col_DH_q2_new_plan,
      'adj_current',    s.col_DJ_q2_adj_current,
      'adj_ssp',        s.col_DK_q2_adj_ssp,
      'adj_rf',         s.col_DL_q2_adj_rf,
      'adj_reserve',    s.col_DM_q2_adj_reserve,
      'adj_comment',    s.col_DN_q2_adj_comment,
      'corrected_plan', s.col_DO_q2_corrected_plan,
      'pay_date',       s.col_DQ_q2_pay_date,
      'pay_amount',     s.col_DR_q2_pay_amount,
      'pay_ho',         s.col_DS_q2_pay_ho,
      'pay_rf',         s.col_DT_q2_pay_rf,
      'pay_comment',    s.col_DU_q2_pay_comment,
      'pay_act',        s.col_DV_q2_pay_act,
      'booking',        s.col_DX_q2_booking,
      'actual_m1',      s.col_DY_q2_actual_m1,
      'actual_m2',      s.col_DZ_q2_actual_m2,
      'actual_m3',      s.col_EA_q2_actual_m3,
      'actual_quarter', s.col_EB_q2_actual_quarter,
      'rem_booking',    s.col_EC_q2_rem_booking,
      'rem_actual',     s.col_ED_q2_rem_actual,
      'transfer_q3',    s.col_EF_q2_trans_q3,
      'transfer_q4',    s.col_EG_q2_trans_q4,
      'economy',        s.col_EH_q2_economy,
      'total',          s.col_EI_q2_total
    ) END AS q2_data,

    CASE WHEN f.s_q3 THEN jsonb_build_object(
      'rev_eco',        s.col_EK_q3_rev_eco,
      'rev_item',       s.col_EL_q3_rev_item,
      'rev_inc',        s.col_EM_q3_rev_inc,
      'rev_seq',        s.col_EN_q3_rev_seq,
      'rev_comment',    s.col_EO_q3_rev_comment,
      'new_plan',       s.col_EP_q3_new_plan,
      'adj_current',    s.col_ER_q3_adj_current,
      'adj_ssp',        s.col_ES_q3_adj_ssp,
      'adj_rf',         s.col_ET_q3_adj_rf,
      'adj_reserve',    s.col_EU_q3_adj_reserve,
      'adj_comment',    s.col_EV_q3_adj_comment,
      'corrected_plan', s.col_EW_q3_corrected_plan,
      'pay_date',       s.col_EY_q3_pay_date,
      'pay_amount',     s.col_EZ_q3_pay_amount,
      'pay_ho',         s.col_FA_q3_pay_ho,
      'pay_rf',         s.col_FB_q3_pay_rf,
      'pay_comment',    s.col_FC_q3_pay_comment,
      'pay_act',        s.col_FD_q3_pay_act,
      'booking',        s.col_FF_q3_booking,
      'actual_m1',      s.col_FG_q3_actual_m1,
      'actual_m2',      s.col_FH_q3_actual_m2,
      'actual_m3',      s.col_FI_q3_actual_m3,
      'actual_quarter', s.col_FJ_q3_actual_quarter,
      'rem_booking',    s.col_FK_q3_rem_booking,
      'rem_actual',     s.col_FL_q3_rem_actual,
      'transfer_q4',    s.col_FN_q3_trans_q4,
      'economy',        s.col_FO_q3_economy,
      'total',          s.col_FP_q3_total
    ) END AS q3_data,

    CASE WHEN f.s_q4 THEN jsonb_build_object(
      'rev_eco',        s.col_FR_q4_rev_eco,
      'rev_item',       s.col_FS_q4_rev_item,
      'rev_inc',        s.col_FT_q4_rev_inc,
      'rev_seq',        s.col_FU_q4_rev_seq,
      'rev_comment',    s.col_FV_q4_rev_comment,
      'new_plan',       s.col_FW_q4_new_plan,
      'adj_current',    s.col_FY_q4_adj_current,
      'adj_ssp',        s.col_FZ_q4_adj_ssp,
      'adj_rf',         s.col_GA_q4_adj_rf,
      'adj_reserve',    s.col_GB_q4_adj_reserve,
      'adj_comment',    s.col_GC_q4_adj_comment,
      'corrected_plan', s.col_GD_q4_corrected_plan,
      'pay_date',       s.col_GF_q4_pay_date,
      'pay_amount',     s.col_GG_q4_pay_amount,
      'pay_ho',         s.col_GH_q4_pay_ho,
      'pay_rf',         s.col_GI_q4_pay_rf,
      'pay_comment',    s.col_GJ_q4_pay_comment,
      'pay_act',        s.col_GK_q4_pay_act,
      'booking',        s.col_GM_q4_booking,
      'actual_m1',      s.col_GN_q4_actual_m1,
      'actual_m2',      s.col_GO_q4_actual_m2,
      'actual_m3',      s.col_GP_q4_actual_m3,
      'actual_quarter', s.col_GQ_q4_actual_quarter,
      'actual_spod',    s.col_GR_q4_actual_spod,
      'rem_booking',    s.col_GS_q4_rem_booking,
      'rem_actual',     s.col_GT_q4_rem_actual,
      'economy',        s.col_GV_q4_economy,
      'total',          s.col_GW_q4_total
    ) END AS q4_data,

    CASE WHEN f.s_tot THEN jsonb_build_object(
      'fact_year', s.col_GZ_fact_year,
      'pay_year',  s.col_HB_pay_year
    ) END AS totals_data,

    s._sort_path
  FROM v_form1_sheet_sections(p_form_id, p_sheet, p_direction, p_sections) s
  CROSS JOIN flags f
  ORDER BY s._sort_path;
$function$
;

-- DROP FUNCTION v3.v_form1_smeta(int4, int4);

CREATE OR REPLACE FUNCTION v3.v_form1_smeta(p_year integer, p_org_unit_id integer)
 RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, supp_plan_q1 numeric, supp_plan_q2 numeric, supp_plan_q3 numeric, supp_plan_q4 numeric, supp_plan_year numeric, dev_plan_q1 numeric, dev_plan_q2 numeric, dev_plan_q3 numeric, dev_plan_q4 numeric, dev_plan_year numeric, total_plan_year numeric, supp_appr_q1 numeric, supp_appr_q2 numeric, supp_appr_q3 numeric, supp_appr_q4 numeric, supp_appr_year numeric, dev_appr_q1 numeric, dev_appr_q2 numeric, dev_appr_q3 numeric, dev_appr_q4 numeric, dev_appr_year numeric, total_appr_year numeric, supp_act_q1 numeric, supp_act_q2 numeric, supp_act_q3 numeric, supp_act_q4 numeric, supp_act_year numeric, dev_act_q1 numeric, dev_act_q2 numeric, dev_act_q3 numeric, dev_act_q4 numeric, dev_act_year numeric, total_act_year numeric, supp_corr_q2 numeric, supp_corr_q3 numeric, supp_corr_q4 numeric, dev_corr_q2 numeric, dev_corr_q3 numeric, dev_corr_q4 numeric)
 LANGUAGE plpgsql
 STABLE
 SET search_path TO 'v3', 'pg_catalog'
AS $function$
#variable_conflict use_column
DECLARE
  v_ahr INT; v_cap INT; v_oper INT;
  v_sections TEXT[] := ARRAY['plan','approved','q1','q2','q3','q4'];
BEGIN
  -- Одна budget_form на лист содержит ОБА направления (budget_line.direction).
  -- Support/Development различаются параметром p_direction, а не отдельной формой.
  WITH ranked AS (
    SELECT bf.id AS fid, ei.sheet AS sh
    FROM budget_form bf
    JOIN budget_line bl ON bl.budget_form_id = bf.id
    JOIN expense_item ei ON ei.id = bl.expense_item_id
    WHERE bf.year = p_year AND bf.org_unit_id = p_org_unit_id
    GROUP BY bf.id, ei.sheet
  )
  SELECT
    MAX(CASE WHEN sh='AHR'  THEN fid END),
    MAX(CASE WHEN sh='CAP'  THEN fid END),
    MAX(CASE WHEN sh='OPER' THEN fid END)
  INTO v_ahr, v_cap, v_oper
  FROM ranked;

  RETURN QUERY
  WITH
    ahr_s AS (
      SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
             col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
             col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
             col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
             COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
             col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
      FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Support', v_sections)
      WHERE row_type IN ('ROOT','GROUP','ITEM')
    ),
    ahr_d AS (
      SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
             col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
             col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
             col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
             COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
             col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
      FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Development', v_sections)
      WHERE row_type IN ('ROOT','GROUP','ITEM')
    ),
    cap_s AS (
      SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
             col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
             col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
             col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
             COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
             col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
      FROM v_form1_sheet_sections(v_cap, 'CAP', 'Support', v_sections)
      WHERE row_type IN ('ROOT','GROUP','ITEM')
    ),
    cap_d AS (
      SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
             col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
             col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
             col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
             COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
             col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
      FROM v_form1_sheet_sections(v_cap, 'CAP', 'Development', v_sections)
      WHERE row_type IN ('ROOT','GROUP','ITEM')
    ),
    oper_s AS (
      SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
             col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
             col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
             col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
             COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
             col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
      FROM v_form1_sheet_sections(v_oper, 'OPER', 'Support', v_sections)
      WHERE row_type IN ('ROOT','GROUP','ITEM')
    ),
    merged AS (
      SELECT COALESCE(s.row_type, d.row_type) AS row_type,
             COALESCE(s.depth, d.depth) AS depth,
             COALESCE(s.sc, d.sc) AS sc,
             COALESCE(s.nm, d.nm) AS nm,
             s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
             d.pq1 AS d_pq1, d.pq2 AS d_pq2, d.pq3 AS d_pq3, d.pq4 AS d_pq4, d.pyr AS d_pyr,
             s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
             d.aq1 AS d_aq1, d.aq2 AS d_aq2, d.aq3 AS d_aq3, d.aq4 AS d_aq4, d.ayr AS d_ayr,
             s.fq1, s.fq2, s.fq3, s.fq4,
             d.fq1 AS d_fq1, d.fq2 AS d_fq2, d.fq3 AS d_fq3, d.fq4 AS d_fq4,
             s.np2, s.np3, s.np4,
             d.np2 AS d_np2, d.np3 AS d_np3, d.np4 AS d_np4
      FROM ahr_s s FULL OUTER JOIN ahr_d d ON s.sc = d.sc
      UNION ALL
      SELECT COALESCE(s.row_type, d.row_type), COALESCE(s.depth, d.depth),
             COALESCE(s.sc, d.sc), COALESCE(s.nm, d.nm),
             s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
             d.pq1, d.pq2, d.pq3, d.pq4, d.pyr,
             s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
             d.aq1, d.aq2, d.aq3, d.aq4, d.ayr,
             s.fq1, s.fq2, s.fq3, s.fq4,
             d.fq1, d.fq2, d.fq3, d.fq4,
             s.np2, s.np3, s.np4,
             d.np2, d.np3, d.np4
      FROM cap_s s FULL OUTER JOIN cap_d d ON s.sc = d.sc
      UNION ALL
      SELECT s.row_type, s.depth, s.sc, s.nm,
             s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
             NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
             s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
             NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
             s.fq1, s.fq2, s.fq3, s.fq4,
             NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
             s.np2, s.np3, s.np4,
             NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC
      FROM oper_s s
    )
  SELECT
    m.row_type, m.depth, m.sc, m.nm,
    m.pq1, m.pq2, m.pq3, m.pq4, m.pyr,
    m.d_pq1, m.d_pq2, m.d_pq3, m.d_pq4, m.d_pyr,
    COALESCE(m.pyr,0) + COALESCE(m.d_pyr,0) AS total_plan_year,
    m.aq1, m.aq2, m.aq3, m.aq4, m.ayr,
    m.d_aq1, m.d_aq2, m.d_aq3, m.d_aq4, m.d_ayr,
    COALESCE(m.ayr,0) + COALESCE(m.d_ayr,0) AS total_appr_year,
    m.fq1, m.fq2, m.fq3, m.fq4, COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0),
    m.d_fq1, m.d_fq2, m.d_fq3, m.d_fq4, COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0),
    COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0)
      + COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0),
    m.np2, m.np3, m.np4,
    m.d_np2, m.d_np3, m.d_np4
  FROM merged m
  ORDER BY m.sc;
END;
$function$
;
-- DROP FUNCTION v3.v_form2_sheet_sections(int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_seq_ssp_q1 numeric, col_seq_ssp_q2 numeric, col_seq_ssp_q3 numeric, col_seq_ssp_q4 numeric, col_seq_ssp_year numeric, col_seq_ssp_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_vat_rate character varying, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, col_pay_year numeric, col_economy_year numeric, _sort_path integer[])
 LANGUAGE plpgsql
 STABLE
AS $function$
#variable_conflict use_column
DECLARE
  s_plan BOOL; s_seq_d BOOL; s_seq_s BOOL; s_appr BOOL;
  s_cd BOOL; s_book BOOL;
  s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL;
  s_need_ap BOOL;  -- plan/seq/reserve нужны для approved + cp1..cp4
BEGIN
  s_plan  := p_sections IS NULL OR 'plan'             = ANY(p_sections);
  s_seq_d := p_sections IS NULL OR 'seq_dfip'         = ANY(p_sections);
  s_seq_s := p_sections IS NULL OR 'seq_ssp'          = ANY(p_sections);
  s_appr  := p_sections IS NULL OR 'approved'         = ANY(p_sections);
  s_cd    := p_sections IS NULL OR 'contract'         = ANY(p_sections);
  s_book  := p_sections IS NULL OR 'booking'          = ANY(p_sections);
  s_q1    := p_sections IS NULL OR 'q1'               = ANY(p_sections);
  s_q2    := p_sections IS NULL OR 'q2'               = ANY(p_sections);
  s_q3    := p_sections IS NULL OR 'q3'               = ANY(p_sections);
  s_q4    := p_sections IS NULL OR 'q4'               = ANY(p_sections);
  s_tot   := p_sections IS NULL OR 'totals'           = ANY(p_sections);
  s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4;

  RETURN QUERY
  WITH
    tw AS (
      SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name,
             t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids
      FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id
      JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code
      WHERE t.sheet = p_sheet
    ),
    page AS (
      SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.name AS bname, bl.vsp_id
      FROM v3.budget_line bl
      JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
      WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
    ),
    -- Conditional joins (gated by section flags)
    jp   AS (SELECT p.* FROM v3.plan p JOIN page pg ON p.line_id = pg.lid WHERE s_need_ap),
    jsd  AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='DFIP'),
    jsg  AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='SSP_GO'),
    jr   AS (SELECT r.* FROM v3.reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_need_ap),
    jcd  AS (SELECT c.* FROM v3.contract_detail c JOIN page pg ON c.line_id = pg.lid WHERE s_cd),
    jck  AS (SELECT c.* FROM v3.ckk c JOIN page pg ON c.line_id = pg.lid WHERE s_book),
    jq1  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q1 AND q.quarter=1),
    jq2  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q2 AND q.quarter=2),
    jq3  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q3 AND q.quarter=3),
    jq4  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE (s_q4 OR s_tot) AND q.quarter=4),

    -- INPUT строки: enriched данные
    input_base AS (
      SELECT
        pg.lid, pg.eid, pg.bname, pg.vsp_id,
        v.address AS vsp_addr,
        t.parent_item_id AS sc, t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, t.path AS tree_path,
        -- plan
        COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2,
        COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4,
        p.comment AS pcmt,
        -- seq dfip / ssp_go
        COALESCE(sd.adj_q1,0) AS dq1, COALESCE(sd.adj_q2,0) AS dq2,
        COALESCE(sd.adj_q3,0) AS dq3, COALESCE(sd.adj_q4,0) AS dq4,
        sd.justification AS djust,
        COALESCE(sg.adj_q1,0) AS gq1, COALESCE(sg.adj_q2,0) AS gq2,
        COALESCE(sg.adj_q3,0) AS gq3, COALESCE(sg.adj_q4,0) AS gq4,
        sg.justification AS gjust,
        -- reserve
        COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2,
        COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4,
        -- contract
        cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.contract_date AS cd_dt,
        cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil,
        cd.vat_rate AS cd_vat, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch,
        cd.act AS cd_act, cd.comment AS cd_cmt,
        -- ckk (booking)
        ck.expenses_q1 AS bk1, ck.expenses_q2 AS bk2, ck.expenses_q3 AS bk3, ck.expenses_q4 AS bk4,
        ck.expenses_next_year_q1 AS bn1, ck.expenses_next_year_q2 AS bn2,
        ck.expenses_next_year_q3 AS bn3, ck.expenses_next_year_q4 AS bn4,
        -- Q1
        q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt,
        q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa,
        q1.payment_comment AS q1_pcmt,
        q1.payment_act AS q1_pact, q1.booking_amount AS q1_book,
        q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3,
        q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4,
        q1.transfer_to_economy AS q1_te,
        q1.transfer_delay_acts AS q1_tda, q1.transfer_delay_procurement AS q1_tdp,
        q1.transfer_economy_rf AS q1_terf,
        q1.transfer_next_comment AS q1_tnc, q1.transfer_far_comment AS q1_tfc,
        -- Q2
        q2.target_change AS q2_tc, q2.base_plan_correction AS q2_bc, q2.base_plan_correction_comment AS q2_bcc,
        q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq,
        q2.plan_revision_comment AS q2_rcmt,
        q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_reserve AS q2_arv,
        q2.adj_comment AS q2_acmt,
        q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa,
        q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact,
        q2.booking_amount AS q2_book,
        q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3,
        q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, q2.transfer_to_economy AS q2_te,
        q2.transfer_delay_acts AS q2_tda, q2.transfer_delay_procurement AS q2_tdp,
        q2.transfer_economy_rf AS q2_terf,
        q2.transfer_next_comment AS q2_tnc, q2.transfer_far_comment AS q2_tfc,
        -- Q3
        q3.target_change AS q3_tc, q3.base_plan_correction AS q3_bc,
        q3.base_plan_correction_comment AS q3_bcc,
        q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq,
        q3.plan_revision_comment AS q3_rcmt,
        q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_reserve AS q3_arv,
        q3.adj_comment AS q3_acmt,
        q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa,
        q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact,
        q3.booking_amount AS q3_book,
        q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3,
        q3.transfer_to_q4 AS q3_tq4, q3.transfer_to_economy AS q3_te,
        q3.transfer_delay_acts AS q3_tda, q3.transfer_delay_procurement AS q3_tdp,
        q3.transfer_economy_rf AS q3_terf, q3.transfer_next_comment AS q3_tnc,
        -- Q4
        q4.target_change AS q4_tc, q4.base_plan_correction AS q4_bc,
        q4.base_plan_correction_comment AS q4_bcc,
        q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq,
        q4.plan_revision_comment AS q4_rcmt,
        q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_reserve AS q4_arv,
        q4.adj_comment AS q4_acmt,
        q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa,
        q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact,
        q4.booking_amount AS q4_book,
        q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod,
        q4.transfer_to_economy AS q4_te,
        -- approved per quarter
        COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(sg.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1,
        COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(sg.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2,
        COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(sg.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3,
        COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(sg.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4
      FROM page pg
      JOIN tw t ON t.id = pg.eid
      LEFT JOIN v3.vsp v ON v.id = pg.vsp_id
      LEFT JOIN jp p   ON p.line_id  = pg.lid
      LEFT JOIN jsd sd ON sd.line_id = pg.lid
      LEFT JOIN jsg sg ON sg.line_id = pg.lid
      LEFT JOIN jr r   ON r.line_id  = pg.lid
      LEFT JOIN jcd cd ON cd.line_id = pg.lid
      LEFT JOIN jck ck ON ck.line_id = pg.lid
      LEFT JOIN jq1 q1 ON q1.line_id = pg.lid
      LEFT JOIN jq2 q2 ON q2.line_id = pg.lid
      LEFT JOIN jq3 q3 ON q3.line_id = pg.lid
      LEFT JOIN jq4 q4 ON q4.line_id = pg.lid
    ),
    -- Computed: corrected/new plans
    enriched AS (
      SELECT b.*,
        -- Q1 corrected_plan = approved + adj_*
        b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_arv,0) AS cp1,
        COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1,
        -- Q2 new_plan = approved + transfer_q1 + revisions(inc/seq) + base_correction
        b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) + COALESCE(b.q2_bc,0) AS np2,
        COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2,
        b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) + COALESCE(b.q3_bc,0) AS np3,
        COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3,
        b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) + COALESCE(b.q4_bc,0) AS np4,
        COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4
      FROM input_base b
    ),
    final_input AS (
      SELECT e.*,
        e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_arv,0) AS cp2,
        e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_arv,0) AS cp3,
        e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_arv,0) AS cp4
      FROM enriched e
    ),

    -- Aggregates per expense_item (для иерархии)
    agg AS (
      SELECT bl.expense_item_id AS eid,
        SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4,
        SUM(sd.adj_q1) AS sd1, SUM(sd.adj_q2) AS sd2, SUM(sd.adj_q3) AS sd3, SUM(sd.adj_q4) AS sd4,
        SUM(sg.adj_q1) AS sg1, SUM(sg.adj_q2) AS sg2, SUM(sg.adj_q3) AS sg3, SUM(sg.adj_q4) AS sg4,
        SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4,
        SUM(ck.expenses_q1) AS bk1, SUM(ck.expenses_q2) AS bk2, SUM(ck.expenses_q3) AS bk3, SUM(ck.expenses_q4) AS bk4,
        SUM(ck.expenses_next_year_q1) AS bn1, SUM(ck.expenses_next_year_q2) AS bn2,
        SUM(ck.expenses_next_year_q3) AS bn3, SUM(ck.expenses_next_year_q4) AS bn4
      FROM v3.budget_line bl
      JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
      LEFT JOIN v3.plan p ON p.line_id = bl.id AND s_need_ap
      LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor='DFIP' AND s_need_ap
      LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap
      LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap
      LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book
      WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
        AND (s_need_ap OR s_book)
      GROUP BY bl.expense_item_id
    ),
    -- Per-quarter aggregates from blq
    aq_q AS (
      SELECT bl.expense_item_id AS eid, q.quarter,
        SUM(q.adj_current) AS ac, SUM(q.adj_ssp) AS as_v, SUM(q.adj_reserve) AS arv,
        SUM(q.payment_amount) AS pa,
        SUM(q.booking_amount) AS bk,
        SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3,
        SUM(q.actual_spod) AS spod,
        SUM(q.transfer_to_q2) AS tq2, SUM(q.transfer_to_q3) AS tq3, SUM(q.transfer_to_q4) AS tq4,
        SUM(q.transfer_to_economy) AS te,
        SUM(q.transfer_delay_acts) AS tda,
        SUM(q.transfer_delay_procurement) AS tdp,
        SUM(q.transfer_economy_rf) AS terf,
        -- Δ к approved для перехода к new_plan: revision_inc/seq + target_change + base_correction
        -- (Excel: DJ10 = SUM(AN, CY, DH, CF, DE:DG)). Должно совпадать с INPUT-формулой np.
        SUM(COALESCE(q.plan_revision_increase,0)
          + COALESCE(q.plan_revision_sequester,0)
          + COALESCE(q.target_change,0)
          + COALESCE(q.base_plan_correction,0)) AS rev
      FROM v3.budget_line bl
      JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
      JOIN v3.budget_line_quarter q ON q.line_id = bl.id
      WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
      GROUP BY bl.expense_item_id, q.quarter
    ),
    -- Tree-rollup
    tw_agg AS (
      SELECT tw.id,
        SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4,
        SUM(a.sd1) AS sd1, SUM(a.sd2) AS sd2, SUM(a.sd3) AS sd3, SUM(a.sd4) AS sd4,
        SUM(a.sg1) AS sg1, SUM(a.sg2) AS sg2, SUM(a.sg3) AS sg3, SUM(a.sg4) AS sg4,
        SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4,
        SUM(a.bk1) AS bk1, SUM(a.bk2) AS bk2, SUM(a.bk3) AS bk3, SUM(a.bk4) AS bk4,
        SUM(a.bn1) AS bn1, SUM(a.bn2) AS bn2, SUM(a.bn3) AS bn3, SUM(a.bn4) AS bn4
      FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids)
      GROUP BY tw.id
    ),
    tw_aq1 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.bk) AS bk,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
        SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te,
        SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=1 GROUP BY tw.id
    ),
    tw_aq2 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.bk) AS bk,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
        SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev,
        SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=2 GROUP BY tw.id
    ),
    tw_aq3 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.bk) AS bk,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
        SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev,
        SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=3 GROUP BY tw.id
    ),
    tw_aq4 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.bk) AS bk,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod,
        SUM(b.te) AS te, SUM(b.rev) AS rev
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=4 GROUP BY tw.id
    )

  -- ═══ Часть A: INPUT строки ═════════════════════════════════════════════
  SELECT * FROM (
    SELECT
      'INPUT'::VARCHAR, 3, f.lid::INT,
      f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.vsp_id, f.vsp_addr,
      -- plan
      CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END,
      CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END,
      CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END,
      -- seq dfip
      CASE WHEN s_seq_d THEN f.dq1 END, CASE WHEN s_seq_d THEN f.dq2 END,
      CASE WHEN s_seq_d THEN f.dq3 END, CASE WHEN s_seq_d THEN f.dq4 END,
      CASE WHEN s_seq_d THEN f.dq1+f.dq2+f.dq3+f.dq4 END, CASE WHEN s_seq_d THEN f.djust END,
      -- seq ssp_go
      CASE WHEN s_seq_s THEN f.gq1 END, CASE WHEN s_seq_s THEN f.gq2 END,
      CASE WHEN s_seq_s THEN f.gq3 END, CASE WHEN s_seq_s THEN f.gq4 END,
      CASE WHEN s_seq_s THEN f.gq1+f.gq2+f.gq3+f.gq4 END, CASE WHEN s_seq_s THEN f.gjust END,
      -- approved
      CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END,
      CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END,
      CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END,
      -- contract
      CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END,
      CASE WHEN s_cd THEN f.cd_dt END, CASE WHEN s_cd THEN f.cd_subj END,
      CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END,
      CASE WHEN s_cd THEN f.cd_vat END, CASE WHEN s_cd THEN f.cd_dl END,
      CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END,
      CASE WHEN s_cd THEN f.cd_cmt END,
      -- booking 2026
      CASE WHEN s_book THEN f.bk1 END, CASE WHEN s_book THEN f.bk2 END,
      CASE WHEN s_book THEN f.bk3 END, CASE WHEN s_book THEN f.bk4 END,
      -- booking 2027
      CASE WHEN s_book THEN f.bn1 END, CASE WHEN s_book THEN f.bn2 END,
      CASE WHEN s_book THEN f.bn3 END, CASE WHEN s_book THEN f.bn4 END,
      -- Q1
      CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, CASE WHEN s_q1 THEN f.q1_arv END,
      CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END,
      CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END,
      CASE WHEN s_q1 THEN f.q1_pcmt END,
      CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN f.q1_book END,
      CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END,
      CASE WHEN s_q1 THEN f.aq1 END,
      CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.q1_book,0) END,    -- residual_after_booking
      CASE WHEN s_q1 THEN f.cp1 - f.aq1 END,                    -- residual_after_actual
      CASE WHEN s_q1 THEN f.q1_tq2 END,
      CASE WHEN s_q1 THEN f.q1_tda END, CASE WHEN s_q1 THEN f.q1_tdp END, CASE WHEN s_q1 THEN f.q1_terf END,
      CASE WHEN s_q1 THEN f.q1_tnc END,
      CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END,
      CASE WHEN s_q1 THEN f.q1_tfc END,
      CASE WHEN s_q1 THEN f.q1_te END,
      -- total = SUM transfer_q2_reasons + transfer_q3 + transfer_q4 + transfer_econ (Excel CC5)
      CASE WHEN s_q1 THEN
        COALESCE(f.q1_tda,0)+COALESCE(f.q1_tdp,0)+COALESCE(f.q1_terf,0)
        +COALESCE(f.q1_tq3,0)+COALESCE(f.q1_tq4,0)+COALESCE(f.q1_te,0)
      END,
      -- Q2
      CASE WHEN s_q2 THEN f.q2_tc END, CASE WHEN s_q2 THEN f.q2_bc END, CASE WHEN s_q2 THEN f.q2_bcc END,
      CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END,
      CASE WHEN s_q2 THEN f.q2_rcmt END,
      CASE WHEN s_q2 THEN f.np2 END,
      CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, CASE WHEN s_q2 THEN f.q2_arv END,
      CASE WHEN s_q2 THEN f.q2_acmt END,
      CASE WHEN s_q2 THEN f.cp2 END,
      CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END,
      CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END,
      CASE WHEN s_q2 THEN f.q2_book END,
      CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END,
      CASE WHEN s_q2 THEN f.aq2 END,
      CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.q2_book,0) END,
      CASE WHEN s_q2 THEN f.cp2 - f.aq2 END,
      CASE WHEN s_q2 THEN f.q2_tq3 END,
      CASE WHEN s_q2 THEN f.q2_tda END, CASE WHEN s_q2 THEN f.q2_tdp END, CASE WHEN s_q2 THEN f.q2_terf END,
      CASE WHEN s_q2 THEN f.q2_tnc END,
      CASE WHEN s_q2 THEN f.q2_tq4 END,
      CASE WHEN s_q2 THEN f.q2_tfc END,
      CASE WHEN s_q2 THEN f.q2_te END,
      -- total = SUM transfer_q3_reasons + transfer_q4 + transfer_econ (Excel DN5)
      CASE WHEN s_q2 THEN
        COALESCE(f.q2_tda,0)+COALESCE(f.q2_tdp,0)+COALESCE(f.q2_terf,0)
        +COALESCE(f.q2_tq4,0)+COALESCE(f.q2_te,0)
      END,
      -- Q3
      CASE WHEN s_q3 THEN f.q3_tc END, CASE WHEN s_q3 THEN f.q3_bc END,
      CASE WHEN s_q3 THEN f.q3_bcc END,
      CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END,
      CASE WHEN s_q3 THEN f.q3_rcmt END,
      CASE WHEN s_q3 THEN f.np3 END,
      CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, CASE WHEN s_q3 THEN f.q3_arv END,
      CASE WHEN s_q3 THEN f.q3_acmt END,
      CASE WHEN s_q3 THEN f.cp3 END,
      CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END,
      CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END,
      CASE WHEN s_q3 THEN f.q3_book END,
      CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END,
      CASE WHEN s_q3 THEN f.aq3 END,
      CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.q3_book,0) END,
      CASE WHEN s_q3 THEN f.cp3 - f.aq3 END,
      CASE WHEN s_q3 THEN f.q3_tq4 END,
      CASE WHEN s_q3 THEN f.q3_tda END, CASE WHEN s_q3 THEN f.q3_tdp END, CASE WHEN s_q3 THEN f.q3_terf END,
      CASE WHEN s_q3 THEN f.q3_tnc END,
      CASE WHEN s_q3 THEN f.q3_te END,
      -- total = SUM transfer_q4_reasons + transfer_econ (Excel EW5)
      CASE WHEN s_q3 THEN
        COALESCE(f.q3_tda,0)+COALESCE(f.q3_tdp,0)+COALESCE(f.q3_terf,0)
        +COALESCE(f.q3_te,0)
      END,
      -- Q4
      CASE WHEN s_q4 THEN f.q4_tc END, CASE WHEN s_q4 THEN f.q4_bc END,
      CASE WHEN s_q4 THEN f.q4_bcc END,
      CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END,
      CASE WHEN s_q4 THEN f.q4_rcmt END,
      CASE WHEN s_q4 THEN f.np4 END,
      CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, CASE WHEN s_q4 THEN f.q4_arv END,
      CASE WHEN s_q4 THEN f.q4_acmt END,
      CASE WHEN s_q4 THEN f.cp4 END,
      CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END,
      CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END,
      CASE WHEN s_q4 THEN f.q4_book END,
      CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END,
      CASE WHEN s_q4 THEN f.q4_spod END,
      CASE WHEN s_q4 THEN f.aq4 END,
      CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.q4_book,0) END,
      CASE WHEN s_q4 THEN f.cp4 - f.aq4 END,
      CASE WHEN s_q4 THEN f.q4_te END,
      -- q4.total = q4.transfer_econ (Q4 has no other outgoing transfers)
      CASE WHEN s_q4 THEN f.q4_te END,
      -- totals
      CASE WHEN s_tot THEN f.aq1+f.aq2+f.aq3+f.aq4 END,
      CASE WHEN s_tot THEN COALESCE(f.q1_pa,0)+COALESCE(f.q2_pa,0)+COALESCE(f.q3_pa,0)+COALESCE(f.q4_pa,0) END,
      -- economy_year = q4.corrected_plan − q4.actual_quarter (Excel GA5 = FY5 = FK5 − FW5)
      CASE WHEN s_tot THEN f.cp4 - f.aq4 END,

      f.tree_path || ARRAY[f.lid::INT] AS _sort
    FROM final_input f

    UNION ALL

    -- ═══ Часть B: Иерархия ════════════════════════════════════════════════
    SELECT
      CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
      t.depth, NULL::INT,
      CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
      CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END,
      t.num_group_id, t.name, NULL::INT, NULL::VARCHAR,
      -- plan
      CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END,
      CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END,
      CASE WHEN s_plan THEN COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) END,
      NULL::VARCHAR,
      -- seq dfip
      CASE WHEN s_seq_d THEN ta.sd1 END, CASE WHEN s_seq_d THEN ta.sd2 END,
      CASE WHEN s_seq_d THEN ta.sd3 END, CASE WHEN s_seq_d THEN ta.sd4 END,
      CASE WHEN s_seq_d THEN COALESCE(ta.sd1,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sd4,0) END,
      NULL::VARCHAR,
      -- seq ssp_go
      CASE WHEN s_seq_s THEN ta.sg1 END, CASE WHEN s_seq_s THEN ta.sg2 END,
      CASE WHEN s_seq_s THEN ta.sg3 END, CASE WHEN s_seq_s THEN ta.sg4 END,
      CASE WHEN s_seq_s THEN COALESCE(ta.sg1,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sg4,0) END,
      NULL::VARCHAR,
      -- approved
      CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)
                          +COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)
                          +COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)
                          +COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END,
      -- contract — нет агрегации (текстовые)
      NULL::VARCHAR, NULL::VARCHAR, NULL::DATE, NULL::VARCHAR,
      NULL::VARCHAR, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR,
      NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
      -- booking 2026 / 2027
      CASE WHEN s_book THEN ta.bk1 END, CASE WHEN s_book THEN ta.bk2 END,
      CASE WHEN s_book THEN ta.bk3 END, CASE WHEN s_book THEN ta.bk4 END,
      CASE WHEN s_book THEN ta.bn1 END, CASE WHEN s_book THEN ta.bn2 END,
      CASE WHEN s_book THEN ta.bn3 END, CASE WHEN s_book THEN ta.bn4 END,
      -- Q1 hierarchy: новые поля (pay_act, booking, residuals, transfer reasons,
      -- transfer comments) на агрегатах NULL'им — их корректный rollup можно
      -- добавить позже расширением aq_q.
      CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as_v END, CASE WHEN s_q1 THEN tb1.arv END,
      NULL::VARCHAR,
      CASE WHEN s_q1 THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0) END,
      NULL::DATE,
      CASE WHEN s_q1 THEN tb1.pa END,
      NULL::VARCHAR,                                            -- pay_comment
      NULL::VARCHAR,                                            -- pay_act
      CASE WHEN s_q1 THEN tb1.bk END,                           -- booking (rollup)
      CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END,
      CASE WHEN s_q1 THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) END,
      -- residual_after_booking = q1.cp − Σ booking
      CASE WHEN s_q1 THEN
        (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0))
        - COALESCE(tb1.bk,0)
      END,
      -- residual_after_actual = q1.cp − Σ actual
      CASE WHEN s_q1 THEN
        (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0))
        - (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0))
      END,
      CASE WHEN s_q1 THEN tb1.tq2 END,
      CASE WHEN s_q1 THEN tb1.tda END, CASE WHEN s_q1 THEN tb1.tdp END, CASE WHEN s_q1 THEN tb1.terf END,
      NULL::VARCHAR,                                            -- transfer_next_comment (text, no rollup)
      CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END,
      NULL::VARCHAR,                                             -- far comment
      CASE WHEN s_q1 THEN tb1.te END,
      -- total = SUM transfer_q2_reasons + transfer_q3 + transfer_q4 + transfer_econ (Excel CC)
      CASE WHEN s_q1 THEN
        COALESCE(tb1.tda,0)+COALESCE(tb1.tdp,0)+COALESCE(tb1.terf,0)
        +COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)+COALESCE(tb1.te,0)
      END,
      -- Q2 hierarchy
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,    -- target_change/base_correction
      NULL::NUMERIC, NULL::NUMERIC,                   -- revision_inc/revision_seq
      NULL::VARCHAR,                                  -- revision_comment
      CASE WHEN s_q2 THEN
        COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)
      END,
      CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as_v END, CASE WHEN s_q2 THEN tb2.arv END,
      NULL::VARCHAR,                                  -- adj_comment
      CASE WHEN s_q2 THEN
        COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0)
      END,
      NULL::DATE,
      CASE WHEN s_q2 THEN tb2.pa END,
      NULL::VARCHAR,                                  -- pay_comment
      NULL::VARCHAR,                                  -- pay_act
      CASE WHEN s_q2 THEN tb2.bk END,                 -- booking (rollup)
      CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END,
      CASE WHEN s_q2 THEN COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) END,
      -- residual_after_booking = q2.cp − Σ booking
      CASE WHEN s_q2 THEN
        (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0))
        - COALESCE(tb2.bk,0)
      END,
      -- residual_after_actual = q2.cp − Σ actual
      CASE WHEN s_q2 THEN
        (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0))
        - (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0))
      END,
      CASE WHEN s_q2 THEN tb2.tq3 END,
      CASE WHEN s_q2 THEN tb2.tda END, CASE WHEN s_q2 THEN tb2.tdp END, CASE WHEN s_q2 THEN tb2.terf END,
      NULL::VARCHAR,                                  -- transfer_next_comment (text, no rollup)
      CASE WHEN s_q2 THEN tb2.tq4 END,
      NULL::VARCHAR,                                  -- far comment
      CASE WHEN s_q2 THEN tb2.te END,
      -- total = SUM transfer_q3_reasons + transfer_q4 + transfer_econ (Excel DN)
      CASE WHEN s_q2 THEN
        COALESCE(tb2.tda,0)+COALESCE(tb2.tdp,0)+COALESCE(tb2.terf,0)
        +COALESCE(tb2.tq4,0)+COALESCE(tb2.te,0)
      END,
      -- Q3 hierarchy
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,    -- target_change/base_correction/comment
      NULL::NUMERIC, NULL::NUMERIC,                   -- revision_inc/revision_seq
      NULL::VARCHAR,
      CASE WHEN s_q3 THEN
        COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)
      END,
      CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as_v END, CASE WHEN s_q3 THEN tb3.arv END,
      NULL::VARCHAR,
      CASE WHEN s_q3 THEN
        COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0)
      END,
      NULL::DATE,
      CASE WHEN s_q3 THEN tb3.pa END,
      NULL::VARCHAR,                                  -- pay_comment
      NULL::VARCHAR,                                  -- pay_act
      CASE WHEN s_q3 THEN tb3.bk END,                 -- booking (rollup)
      CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END,
      CASE WHEN s_q3 THEN COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) END,
      -- residual_after_booking = q3.cp − Σ booking
      CASE WHEN s_q3 THEN
        (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0))
        - COALESCE(tb3.bk,0)
      END,
      -- residual_after_actual = q3.cp − Σ actual
      CASE WHEN s_q3 THEN
        (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0))
        - (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0))
      END,
      CASE WHEN s_q3 THEN tb3.tq4 END,
      CASE WHEN s_q3 THEN tb3.tda END, CASE WHEN s_q3 THEN tb3.tdp END, CASE WHEN s_q3 THEN tb3.terf END,
      NULL::VARCHAR,                                  -- transfer_next_comment (text, no rollup)
      CASE WHEN s_q3 THEN tb3.te END,
      -- total = SUM transfer_q4_reasons + transfer_econ (Excel EW)
      CASE WHEN s_q3 THEN
        COALESCE(tb3.tda,0)+COALESCE(tb3.tdp,0)+COALESCE(tb3.terf,0)
        +COALESCE(tb3.te,0)
      END,
      -- Q4 hierarchy
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,    -- target_change/base_correction/comment
      NULL::NUMERIC, NULL::NUMERIC,                   -- revision_inc/revision_seq
      NULL::VARCHAR,
      CASE WHEN s_q4 THEN
        COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)
      END,
      CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as_v END, CASE WHEN s_q4 THEN tb4.arv END,
      NULL::VARCHAR,
      CASE WHEN s_q4 THEN
        COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)
      END,
      NULL::DATE,
      CASE WHEN s_q4 THEN tb4.pa END,
      NULL::VARCHAR,                                  -- pay_comment
      NULL::VARCHAR,                                  -- pay_act
      CASE WHEN s_q4 THEN tb4.bk END,                 -- booking (rollup)
      CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END,
      CASE WHEN s_q4 THEN tb4.spod END,
      CASE WHEN s_q4 THEN COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END,
      -- residual_after_booking = q4.cp − Σ booking
      CASE WHEN s_q4 THEN
        (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0))
        - COALESCE(tb4.bk,0)
      END,
      -- residual_after_actual = q4.cp − Σ actual (incl. spod)
      CASE WHEN s_q4 THEN
        (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0))
        - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0))
      END,
      CASE WHEN s_q4 THEN tb4.te END,
      -- q4.total = q4.transfer_econ (Q4 has no other outgoing transfers)
      CASE WHEN s_q4 THEN tb4.te END,
      -- totals
      CASE WHEN s_tot THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)
                          +COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)
                          +COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)
                          +COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END,
      CASE WHEN s_tot THEN COALESCE(tb1.pa,0)+COALESCE(tb2.pa,0)+COALESCE(tb3.pa,0)+COALESCE(tb4.pa,0) END,
      -- economy_year = q4.corrected_plan − q4.actual_quarter (Excel GA = FY = FK − FW)
      CASE WHEN s_tot THEN
        (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)
         +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)
         +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0))
        - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0))
      END,

      t.path AS _sort
    FROM tw t
    LEFT JOIN tw_agg ta ON ta.id = t.id
    LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id
    LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id
    LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id
    LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id
  ) sub
  ORDER BY sub._sort;
END;
$function$
;

-- DROP FUNCTION v3.v_form2_sheet_jsonb(int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form2_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, seq_ssp_data jsonb, approved_data jsonb, contract_data jsonb, booking_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[])
 LANGUAGE sql
 STABLE
AS $function$
  WITH flags AS (
    SELECT
      (p_sections IS NULL OR 'plan'     = ANY(p_sections)) AS s_plan,
      (p_sections IS NULL OR 'seq_dfip' = ANY(p_sections)) AS s_seq_d,
      (p_sections IS NULL OR 'seq_ssp'  = ANY(p_sections)) AS s_seq_s,
      (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr,
      (p_sections IS NULL OR 'contract' = ANY(p_sections)) AS s_cd,
      (p_sections IS NULL OR 'booking'  = ANY(p_sections)) AS s_book,
      (p_sections IS NULL OR 'q1'       = ANY(p_sections)) AS s_q1,
      (p_sections IS NULL OR 'q2'       = ANY(p_sections)) AS s_q2,
      (p_sections IS NULL OR 'q3'       = ANY(p_sections)) AS s_q3,
      (p_sections IS NULL OR 'q4'       = ANY(p_sections)) AS s_q4,
      (p_sections IS NULL OR 'totals'   = ANY(p_sections)) AS s_tot
  )
  SELECT
    s.row_type, s.depth, s.line_id,

    -- header (всегда). expense_item_id из _sort_path (см. v_form1_jsonb).
    jsonb_build_object(
      'section_code',    s.col_section_code,
      'item_id',         s.col_item_id,
      'num_group',       s.col_num_group_id,
      'name',            s.col_name,
      'vsp_id',          s.col_vsp_id,
      'vsp_address',     s.col_vsp_address,
      'expense_item_id', CASE WHEN s.row_type = 'INPUT'
                           THEN s._sort_path[array_upper(s._sort_path,1)-1]
                           ELSE s._sort_path[array_upper(s._sort_path,1)] END
    ) AS header,

    CASE WHEN f.s_plan THEN jsonb_build_object(
      'q1',      s.col_plan_q1, 'q2', s.col_plan_q2,
      'q3',      s.col_plan_q3, 'q4', s.col_plan_q4,
      'year',    s.col_plan_year, 'comment', s.col_plan_comment
    ) END AS plan_data,

    CASE WHEN f.s_seq_d THEN jsonb_build_object(
      'q1',     s.col_seq_dfip_q1, 'q2', s.col_seq_dfip_q2,
      'q3',     s.col_seq_dfip_q3, 'q4', s.col_seq_dfip_q4,
      'year',   s.col_seq_dfip_year,
      'justification', s.col_seq_dfip_just
    ) END AS seq_dfip_data,

    CASE WHEN f.s_seq_s THEN jsonb_build_object(
      'q1',     s.col_seq_ssp_q1, 'q2', s.col_seq_ssp_q2,
      'q3',     s.col_seq_ssp_q3, 'q4', s.col_seq_ssp_q4,
      'year',   s.col_seq_ssp_year,
      'justification', s.col_seq_ssp_just
    ) END AS seq_ssp_data,

    CASE WHEN f.s_appr THEN jsonb_build_object(
      'q1',   s.col_appr_q1, 'q2', s.col_appr_q2,
      'q3',   s.col_appr_q3, 'q4', s.col_appr_q4,
      'year', s.col_appr_year
    ) END AS approved_data,

    CASE WHEN f.s_cd THEN jsonb_build_object(
      'counterparty', s.col_cd_counterparty,
      'reference',    s.col_cd_reference,
      'date',         s.col_cd_date,
      'subject',      s.col_cd_subject,
      'currency',     s.col_cd_currency,
      'ceiling',      s.col_cd_ceiling,
      'vat_rate',     s.col_cd_vat_rate,
      'deadline',     s.col_cd_deadline,
      'scheme',       s.col_cd_scheme,
      'act',          s.col_cd_act,
      'comment',      s.col_cd_comment
    ) END AS contract_data,

    CASE WHEN f.s_book THEN jsonb_build_object(
      'y2026', jsonb_build_object('q1', s.col_book_q1, 'q2', s.col_book_q2, 'q3', s.col_book_q3, 'q4', s.col_book_q4),
      'y2027', jsonb_build_object('q1', s.col_book_next_q1, 'q2', s.col_book_next_q2, 'q3', s.col_book_next_q3, 'q4', s.col_book_next_q4)
    ) END AS booking_data,

    CASE WHEN f.s_q1 THEN jsonb_build_object(
      'adj_current',    s.col_q1_adj_current,
      'adj_ssp',        s.col_q1_adj_ssp,
      'adj_reserve',    s.col_q1_adj_reserve,
      'adj_comment',    s.col_q1_adj_comment,
      'corrected_plan', s.col_q1_corrected_plan,
      'pay_date',       s.col_q1_pay_date,
      'pay_amount',     s.col_q1_pay_amount,
      'pay_comment',    s.col_q1_pay_comment,
      'pay_act',        s.col_q1_pay_act,
      'booking',        s.col_q1_booking,
      'actual_m1',      s.col_q1_actual_m1,
      'actual_m2',      s.col_q1_actual_m2,
      'actual_m3',      s.col_q1_actual_m3,
      'actual_quarter', s.col_q1_actual_quarter,
      'residual_after_booking', s.col_q1_residual_after_booking,
      'residual_after_actual',  s.col_q1_residual_after_actual,
      'transfer_q2',    s.col_q1_transfer_q2,
      'transfer_q2_delay_acts',        s.col_q1_transfer_q2_delay_acts,
      'transfer_q2_delay_procurement', s.col_q1_transfer_q2_delay_procurement,
      'transfer_q2_economy_rf',        s.col_q1_transfer_q2_economy_rf,
      'transfer_next_comment',         s.col_q1_transfer_next_comment,
      'transfer_q3',    s.col_q1_transfer_q3,
      'transfer_q4',    s.col_q1_transfer_q4,
      'transfer_far_comment',          s.col_q1_transfer_far_comment,
      'transfer_econ',  s.col_q1_transfer_econ,
      'total',          s.col_q1_total
    ) END AS q1_data,

    CASE WHEN f.s_q2 THEN jsonb_build_object(
      'target_change',           s.col_q2_target_change,
      'base_correction',         s.col_q2_base_correction,
      'base_correction_comment', s.col_q2_base_correction_comment,
      'revision_inc',            s.col_q2_revision_inc,
      'revision_seq',            s.col_q2_revision_seq,
      'revision_comment',        s.col_q2_revision_comment,
      'new_plan',                s.col_q2_new_plan,
      'adj_current',             s.col_q2_adj_current,
      'adj_ssp',                 s.col_q2_adj_ssp,
      'adj_reserve',             s.col_q2_adj_reserve,
      'adj_comment',             s.col_q2_adj_comment,
      'corrected_plan',          s.col_q2_corrected_plan,
      'pay_date',                s.col_q2_pay_date,
      'pay_amount',              s.col_q2_pay_amount,
      'pay_comment',             s.col_q2_pay_comment,
      'pay_act',                 s.col_q2_pay_act,
      'booking',                 s.col_q2_booking,
      'actual_m1',               s.col_q2_actual_m1,
      'actual_m2',               s.col_q2_actual_m2,
      'actual_m3',               s.col_q2_actual_m3,
      'actual_quarter',          s.col_q2_actual_quarter,
      'residual_after_booking',  s.col_q2_residual_after_booking,
      'residual_after_actual',   s.col_q2_residual_after_actual,
      'transfer_q3',             s.col_q2_transfer_q3,
      'transfer_q3_delay_acts',        s.col_q2_transfer_q3_delay_acts,
      'transfer_q3_delay_procurement', s.col_q2_transfer_q3_delay_procurement,
      'transfer_q3_economy_rf',        s.col_q2_transfer_q3_economy_rf,
      'transfer_next_comment',         s.col_q2_transfer_next_comment,
      'transfer_q4',             s.col_q2_transfer_q4,
      'transfer_far_comment',          s.col_q2_transfer_far_comment,
      'transfer_econ',           s.col_q2_transfer_econ,
      'total',                   s.col_q2_total
    ) END AS q2_data,

    CASE WHEN f.s_q3 THEN jsonb_build_object(
      'target_change',           s.col_q3_target_change,
      'base_correction',         s.col_q3_base_correction,
      'base_correction_comment', s.col_q3_base_correction_comment,
      'revision_inc',            s.col_q3_revision_inc,
      'revision_seq',            s.col_q3_revision_seq,
      'revision_comment',        s.col_q3_revision_comment,
      'new_plan',                s.col_q3_new_plan,
      'adj_current',             s.col_q3_adj_current,
      'adj_ssp',                 s.col_q3_adj_ssp,
      'adj_reserve',             s.col_q3_adj_reserve,
      'adj_comment',             s.col_q3_adj_comment,
      'corrected_plan',          s.col_q3_corrected_plan,
      'pay_date',                s.col_q3_pay_date,
      'pay_amount',              s.col_q3_pay_amount,
      'pay_comment',             s.col_q3_pay_comment,
      'pay_act',                 s.col_q3_pay_act,
      'booking',                 s.col_q3_booking,
      'actual_m1',               s.col_q3_actual_m1,
      'actual_m2',               s.col_q3_actual_m2,
      'actual_m3',               s.col_q3_actual_m3,
      'actual_quarter',          s.col_q3_actual_quarter,
      'residual_after_booking',  s.col_q3_residual_after_booking,
      'residual_after_actual',   s.col_q3_residual_after_actual,
      'transfer_q4',             s.col_q3_transfer_q4,
      'transfer_q4_delay_acts',        s.col_q3_transfer_q4_delay_acts,
      'transfer_q4_delay_procurement', s.col_q3_transfer_q4_delay_procurement,
      'transfer_q4_economy_rf',        s.col_q3_transfer_q4_economy_rf,
      'transfer_next_comment',         s.col_q3_transfer_next_comment,
      'transfer_econ',           s.col_q3_transfer_econ,
      'total',                   s.col_q3_total
    ) END AS q3_data,

    CASE WHEN f.s_q4 THEN jsonb_build_object(
      'target_change',           s.col_q4_target_change,
      'base_correction',         s.col_q4_base_correction,
      'base_correction_comment', s.col_q4_base_correction_comment,
      'revision_inc',            s.col_q4_revision_inc,
      'revision_seq',            s.col_q4_revision_seq,
      'revision_comment',        s.col_q4_revision_comment,
      'new_plan',                s.col_q4_new_plan,
      'adj_current',             s.col_q4_adj_current,
      'adj_ssp',                 s.col_q4_adj_ssp,
      'adj_reserve',             s.col_q4_adj_reserve,
      'adj_comment',             s.col_q4_adj_comment,
      'corrected_plan',          s.col_q4_corrected_plan,
      'pay_date',                s.col_q4_pay_date,
      'pay_amount',              s.col_q4_pay_amount,
      'pay_comment',             s.col_q4_pay_comment,
      'pay_act',                 s.col_q4_pay_act,
      'booking',                 s.col_q4_booking,
      'actual_m1',               s.col_q4_actual_m1,
      'actual_m2',               s.col_q4_actual_m2,
      'actual_m3',               s.col_q4_actual_m3,
      'actual_spod',             s.col_q4_actual_spod,
      'actual_quarter',          s.col_q4_actual_quarter,
      'residual_after_booking',  s.col_q4_residual_after_booking,
      'residual_after_actual',   s.col_q4_residual_after_actual,
      'transfer_econ',           s.col_q4_transfer_econ,
      'total',                   s.col_q4_total
    ) END AS q4_data,

    CASE WHEN f.s_tot THEN jsonb_build_object(
      'fact_year',    s.col_fact_year,
      'pay_year',     s.col_pay_year,
      'economy_year', s.col_economy_year
    ) END AS totals_data,

    s._sort_path
  FROM v3.v_form2_sheet_sections(p_form_id, p_sheet, p_sections) s
  CROSS JOIN flags f
  ORDER BY s._sort_path;
$function$
;

-- DROP FUNCTION v3.v_form2_smeta(int4);

CREATE OR REPLACE FUNCTION v3.v_form2_smeta(p_form_id integer)
 RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, plan_supp_q1 numeric, plan_supp_q2 numeric, plan_supp_q3 numeric, plan_supp_q4 numeric, plan_supp_year numeric, plan_dev_q1 numeric, plan_dev_q2 numeric, plan_dev_q3 numeric, plan_dev_q4 numeric, plan_dev_year numeric, appr_supp_q1 numeric, appr_supp_q2 numeric, appr_supp_q3 numeric, appr_supp_q4 numeric, appr_supp_year numeric, appr_dev_q1 numeric, appr_dev_q2 numeric, appr_dev_q3 numeric, appr_dev_q4 numeric, appr_dev_year numeric, fact_supp_q1 numeric, fact_supp_q2 numeric, fact_supp_q3 numeric, fact_supp_q4 numeric, fact_supp_year numeric, fact_dev_q1 numeric, fact_dev_q2 numeric, fact_dev_q3 numeric, fact_dev_q4 numeric, fact_dev_year numeric, corr_supp_q2 numeric, corr_supp_q3 numeric, corr_supp_q4 numeric, corr_dev_q2 numeric, corr_dev_q3 numeric, corr_dev_q4 numeric)
 LANGUAGE plpgsql
 STABLE
AS $function$
#variable_conflict use_column
BEGIN
  RETURN QUERY
  WITH
    -- Per-line per-direction агрегаты квартальных данных
    q_per_line AS (
      SELECT line_id,
        SUM(CASE WHEN quarter=1 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq1,
        SUM(CASE WHEN quarter=2 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq2,
        SUM(CASE WHEN quarter=3 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq3,
        SUM(CASE WHEN quarter=4 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0)+COALESCE(actual_spod,0) END) AS fq4,
        SUM(CASE WHEN quarter=1 THEN transfer_to_q2 END) AS t1q2,
        SUM(CASE WHEN quarter=1 THEN transfer_to_q3 END) AS t1q3,
        SUM(CASE WHEN quarter=1 THEN transfer_to_q4 END) AS t1q4,
        SUM(CASE WHEN quarter=2 THEN transfer_to_q3 END) AS t2q3,
        SUM(CASE WHEN quarter=2 THEN transfer_to_q4 END) AS t2q4,
        SUM(CASE WHEN quarter=3 THEN transfer_to_q4 END) AS t3q4,
        SUM(CASE WHEN quarter=1 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj1,
        SUM(CASE WHEN quarter=2 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj2,
        SUM(CASE WHEN quarter=3 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj3,
        SUM(CASE WHEN quarter=4 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj4,
        SUM(CASE WHEN quarter=2 THEN COALESCE(plan_revision_eco_change,0)+COALESCE(plan_revision_item_adj,0)+COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0) END) AS rev2,
        SUM(CASE WHEN quarter=3 THEN COALESCE(plan_revision_eco_change,0)+COALESCE(plan_revision_item_adj,0)+COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0) END) AS rev3,
        SUM(CASE WHEN quarter=4 THEN COALESCE(plan_revision_eco_change,0)+COALESCE(plan_revision_item_adj,0)+COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0) END) AS rev4
      FROM v3.budget_line_quarter GROUP BY line_id
    ),
    seq_dfip AS (
      SELECT line_id, SUM(adj_q1) AS s1, SUM(adj_q2) AS s2, SUM(adj_q3) AS s3, SUM(adj_q4) AS s4
      FROM v3.sequestration WHERE actor='DFIP' GROUP BY line_id
    ),
    seq_ssp AS (
      SELECT line_id, SUM(adj_q1) AS g1, SUM(adj_q2) AS g2, SUM(adj_q3) AS g3, SUM(adj_q4) AS g4
      FROM v3.sequestration WHERE actor='SSP_GO' GROUP BY line_id
    ),
    -- Per-line aggregate: учёт direction из expense_item
    line_agg AS (
      SELECT
        bl.expense_item_id AS eid, ei.direction AS dir,
        SUM(p.plan_q1) AS pq1, SUM(p.plan_q2) AS pq2, SUM(p.plan_q3) AS pq3, SUM(p.plan_q4) AS pq4,
        SUM(sd.s1) AS sd1, SUM(sd.s2) AS sd2, SUM(sd.s3) AS sd3, SUM(sd.s4) AS sd4,
        SUM(sg.g1) AS sg1, SUM(sg.g2) AS sg2, SUM(sg.g3) AS sg3, SUM(sg.g4) AS sg4,
        SUM(r.amount_q1) AS rq1, SUM(r.amount_q2) AS rq2, SUM(r.amount_q3) AS rq3, SUM(r.amount_q4) AS rq4,
        SUM(qp.fq1) AS fq1, SUM(qp.fq2) AS fq2, SUM(qp.fq3) AS fq3, SUM(qp.fq4) AS fq4,
        SUM(qp.t1q2) AS t1q2, SUM(qp.t1q3) AS t1q3, SUM(qp.t1q4) AS t1q4,
        SUM(qp.t2q3) AS t2q3, SUM(qp.t2q4) AS t2q4, SUM(qp.t3q4) AS t3q4,
        SUM(qp.adj1) AS adj1, SUM(qp.adj2) AS adj2, SUM(qp.adj3) AS adj3, SUM(qp.adj4) AS adj4,
        SUM(qp.rev2) AS rev2, SUM(qp.rev3) AS rev3, SUM(qp.rev4) AS rev4
      FROM v3.budget_line bl
      JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
      LEFT JOIN v3.plan p          ON p.line_id  = bl.id
      LEFT JOIN seq_dfip sd        ON sd.line_id = bl.id
      LEFT JOIN seq_ssp sg         ON sg.line_id = bl.id
      LEFT JOIN v3.reserve r       ON r.line_id  = bl.id
      LEFT JOIN q_per_line qp      ON qp.line_id = bl.id
      WHERE bl.budget_form_id = p_form_id
      GROUP BY bl.expense_item_id, ei.direction
    ),
    -- Tree rollup с расщеплением по direction. Используем FILTER (WHERE)
    tw_rollup AS (
      SELECT
        t.id, t.depth, t.item_id AS section_code, t.name,
        -- Поддержка
        SUM(a.pq1) FILTER (WHERE a.dir='Support') AS s_pq1,
        SUM(a.pq2) FILTER (WHERE a.dir='Support') AS s_pq2,
        SUM(a.pq3) FILTER (WHERE a.dir='Support') AS s_pq3,
        SUM(a.pq4) FILTER (WHERE a.dir='Support') AS s_pq4,
        SUM(a.sd1) FILTER (WHERE a.dir='Support') AS s_sd1,
        SUM(a.sd2) FILTER (WHERE a.dir='Support') AS s_sd2,
        SUM(a.sd3) FILTER (WHERE a.dir='Support') AS s_sd3,
        SUM(a.sd4) FILTER (WHERE a.dir='Support') AS s_sd4,
        SUM(a.sg1) FILTER (WHERE a.dir='Support') AS s_sg1,
        SUM(a.sg2) FILTER (WHERE a.dir='Support') AS s_sg2,
        SUM(a.sg3) FILTER (WHERE a.dir='Support') AS s_sg3,
        SUM(a.sg4) FILTER (WHERE a.dir='Support') AS s_sg4,
        SUM(a.rq1) FILTER (WHERE a.dir='Support') AS s_rq1,
        SUM(a.rq2) FILTER (WHERE a.dir='Support') AS s_rq2,
        SUM(a.rq3) FILTER (WHERE a.dir='Support') AS s_rq3,
        SUM(a.rq4) FILTER (WHERE a.dir='Support') AS s_rq4,
        SUM(a.fq1) FILTER (WHERE a.dir='Support') AS s_fq1,
        SUM(a.fq2) FILTER (WHERE a.dir='Support') AS s_fq2,
        SUM(a.fq3) FILTER (WHERE a.dir='Support') AS s_fq3,
        SUM(a.fq4) FILTER (WHERE a.dir='Support') AS s_fq4,
        SUM(a.t1q2) FILTER (WHERE a.dir='Support') AS s_t1q2,
        SUM(a.t1q3) FILTER (WHERE a.dir='Support') AS s_t1q3,
        SUM(a.t1q4) FILTER (WHERE a.dir='Support') AS s_t1q4,
        SUM(a.t2q3) FILTER (WHERE a.dir='Support') AS s_t2q3,
        SUM(a.t2q4) FILTER (WHERE a.dir='Support') AS s_t2q4,
        SUM(a.t3q4) FILTER (WHERE a.dir='Support') AS s_t3q4,
        SUM(a.adj1) FILTER (WHERE a.dir='Support') AS s_adj1,
        SUM(a.adj2) FILTER (WHERE a.dir='Support') AS s_adj2,
        SUM(a.adj3) FILTER (WHERE a.dir='Support') AS s_adj3,
        SUM(a.adj4) FILTER (WHERE a.dir='Support') AS s_adj4,
        SUM(a.rev2) FILTER (WHERE a.dir='Support') AS s_rev2,
        SUM(a.rev3) FILTER (WHERE a.dir='Support') AS s_rev3,
        SUM(a.rev4) FILTER (WHERE a.dir='Support') AS s_rev4,
        -- Развитие
        SUM(a.pq1) FILTER (WHERE a.dir='Development') AS d_pq1,
        SUM(a.pq2) FILTER (WHERE a.dir='Development') AS d_pq2,
        SUM(a.pq3) FILTER (WHERE a.dir='Development') AS d_pq3,
        SUM(a.pq4) FILTER (WHERE a.dir='Development') AS d_pq4,
        SUM(a.sd1) FILTER (WHERE a.dir='Development') AS d_sd1,
        SUM(a.sd2) FILTER (WHERE a.dir='Development') AS d_sd2,
        SUM(a.sd3) FILTER (WHERE a.dir='Development') AS d_sd3,
        SUM(a.sd4) FILTER (WHERE a.dir='Development') AS d_sd4,
        SUM(a.sg1) FILTER (WHERE a.dir='Development') AS d_sg1,
        SUM(a.sg2) FILTER (WHERE a.dir='Development') AS d_sg2,
        SUM(a.sg3) FILTER (WHERE a.dir='Development') AS d_sg3,
        SUM(a.sg4) FILTER (WHERE a.dir='Development') AS d_sg4,
        SUM(a.rq1) FILTER (WHERE a.dir='Development') AS d_rq1,
        SUM(a.rq2) FILTER (WHERE a.dir='Development') AS d_rq2,
        SUM(a.rq3) FILTER (WHERE a.dir='Development') AS d_rq3,
        SUM(a.rq4) FILTER (WHERE a.dir='Development') AS d_rq4,
        SUM(a.fq1) FILTER (WHERE a.dir='Development') AS d_fq1,
        SUM(a.fq2) FILTER (WHERE a.dir='Development') AS d_fq2,
        SUM(a.fq3) FILTER (WHERE a.dir='Development') AS d_fq3,
        SUM(a.fq4) FILTER (WHERE a.dir='Development') AS d_fq4,
        SUM(a.t1q2) FILTER (WHERE a.dir='Development') AS d_t1q2,
        SUM(a.t1q3) FILTER (WHERE a.dir='Development') AS d_t1q3,
        SUM(a.t1q4) FILTER (WHERE a.dir='Development') AS d_t1q4,
        SUM(a.t2q3) FILTER (WHERE a.dir='Development') AS d_t2q3,
        SUM(a.t2q4) FILTER (WHERE a.dir='Development') AS d_t2q4,
        SUM(a.t3q4) FILTER (WHERE a.dir='Development') AS d_t3q4,
        SUM(a.adj1) FILTER (WHERE a.dir='Development') AS d_adj1,
        SUM(a.adj2) FILTER (WHERE a.dir='Development') AS d_adj2,
        SUM(a.adj3) FILTER (WHERE a.dir='Development') AS d_adj3,
        SUM(a.adj4) FILTER (WHERE a.dir='Development') AS d_adj4,
        SUM(a.rev2) FILTER (WHERE a.dir='Development') AS d_rev2,
        SUM(a.rev3) FILTER (WHERE a.dir='Development') AS d_rev3,
        SUM(a.rev4) FILTER (WHERE a.dir='Development') AS d_rev4
      FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id
      JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code
      LEFT JOIN line_agg a ON a.eid = ANY(t.desc_ids)
      WHERE t.depth <= 2
      GROUP BY t.id, t.depth, t.item_id, t.name
    )
  SELECT
    CASE r.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' END::VARCHAR,
    r.depth,
    r.section_code::VARCHAR,
    r.name::VARCHAR,
    -- Plan supp / dev
    r.s_pq1, r.s_pq2, r.s_pq3, r.s_pq4,
    COALESCE(r.s_pq1,0)+COALESCE(r.s_pq2,0)+COALESCE(r.s_pq3,0)+COALESCE(r.s_pq4,0),
    r.d_pq1, r.d_pq2, r.d_pq3, r.d_pq4,
    COALESCE(r.d_pq1,0)+COALESCE(r.d_pq2,0)+COALESCE(r.d_pq3,0)+COALESCE(r.d_pq4,0),
    -- Approved supp = plan + seq_dfip + seq_ssp + reserve
    COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0),
    COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0),
    COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0),
    COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0),
    COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0)
      +COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0)
      +COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0)
      +COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0),
    -- Approved dev
    COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0),
    COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0),
    COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0),
    COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0),
    COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0)
      +COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0)
      +COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0)
      +COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0),
    -- Fact supp / dev
    r.s_fq1, r.s_fq2, r.s_fq3, r.s_fq4,
    COALESCE(r.s_fq1,0)+COALESCE(r.s_fq2,0)+COALESCE(r.s_fq3,0)+COALESCE(r.s_fq4,0),
    r.d_fq1, r.d_fq2, r.d_fq3, r.d_fq4,
    COALESCE(r.d_fq1,0)+COALESCE(r.d_fq2,0)+COALESCE(r.d_fq3,0)+COALESCE(r.d_fq4,0),
    -- Corrected supp Q2..Q4 = approved_qN + transfers_into_qN + revisions + adj
    COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0)
      +COALESCE(r.s_t1q2,0)+COALESCE(r.s_rev2,0)+COALESCE(r.s_adj2,0),
    COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0)
      +COALESCE(r.s_t1q3,0)+COALESCE(r.s_t2q3,0)+COALESCE(r.s_rev3,0)+COALESCE(r.s_adj3,0),
    COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0)
      +COALESCE(r.s_t1q4,0)+COALESCE(r.s_t2q4,0)+COALESCE(r.s_t3q4,0)+COALESCE(r.s_rev4,0)+COALESCE(r.s_adj4,0),
    -- Corrected dev Q2..Q4
    COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0)
      +COALESCE(r.d_t1q2,0)+COALESCE(r.d_rev2,0)+COALESCE(r.d_adj2,0),
    COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0)
      +COALESCE(r.d_t1q3,0)+COALESCE(r.d_t2q3,0)+COALESCE(r.d_rev3,0)+COALESCE(r.d_adj3,0),
    COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0)
      +COALESCE(r.d_t1q4,0)+COALESCE(r.d_t2q4,0)+COALESCE(r.d_t3q4,0)+COALESCE(r.d_rev4,0)+COALESCE(r.d_adj4,0)
  FROM tw_rollup r
  ORDER BY r.section_code;
END;
$function$
;

-- DROP FUNCTION v3.v_form2_view(int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form2_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
 STABLE
AS $function$
BEGIN

  -- ═══ Основные листы АХР / КВ_П / Операц ════════════════════════════════
  IF p_sheet IN ('AHR','CAP','OPER') THEN
    RETURN QUERY
    SELECT
      j.row_type,
      j.depth,
      ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order,
      jsonb_build_object(
        'line_id',          j.line_id,
        'header',           j.header,
        'plan',             j.plan_data,
        'seq_dfip',         j.seq_dfip_data,
        'seq_ssp',          j.seq_ssp_data,
        'approved',         j.approved_data,
        'contract',         j.contract_data,
        'booking',          j.booking_data,
        'q1',               j.q1_data,
        'q2',               j.q2_data,
        'q3',               j.q3_data,
        'q4',               j.q4_data,
        'totals',           j.totals_data
      ) AS data
    FROM v3.v_form2_sheet_jsonb(p_form_id, p_sheet, p_sections) j
    ORDER BY j._sort_path;
    RETURN;
  END IF;

  -- ═══ АХР_лимит ═════════════════════════════════════════════════════════
  -- SECTION/GROUP-строки агрегируют qty/amount по всем LEAF-потомкам
  -- (Excel: J6=SUM(J7:J13) и т.д.). Дерево строится через parent_id.
  --
  -- Структура (limit_template) — глобальный каталог нормативов, единый на банк.
  -- Значения qty/comment — per-form (form_limit ON budget_form_id = p_form_id).
  -- amount_qN — computed по формуле:
  --   amount_qN = qty_qN * limit_with_vat * period_factor(unit), где
  --   period_factor = 1/4   если unit ~ '/год'
  --                   3     если unit ~ '/месяц' (3 месяца в квартале)
  --                   1     иначе (по умолчанию /квартал)
  IF p_sheet = 'AHR_LIMIT' THEN
    RETURN QUERY
    WITH RECURSIVE tree AS (
      SELECT id AS root_id, id AS desc_id FROM v3.limit_template
      UNION
      SELECT t.root_id, child.id
      FROM tree t
      JOIN v3.limit_template child ON child.parent_id = t.desc_id
    ),
    leaf_amt AS (
      -- per-LEAF computed amount по формуле; qty берётся из form_limit для p_form_id
      SELECT lt.id,
             fl.qty_q1, fl.qty_q2, fl.qty_q3, fl.qty_q4,
             COALESCE(fl.qty_q1,0) * COALESCE(lt.limit_with_vat,0) *
               CASE WHEN lt.unit LIKE '%/год'   THEN 0.25
                    WHEN lt.unit LIKE '%/месяц' THEN 3
                    ELSE 1 END AS amount_q1,
             COALESCE(fl.qty_q2,0) * COALESCE(lt.limit_with_vat,0) *
               CASE WHEN lt.unit LIKE '%/год'   THEN 0.25
                    WHEN lt.unit LIKE '%/месяц' THEN 3
                    ELSE 1 END AS amount_q2,
             COALESCE(fl.qty_q3,0) * COALESCE(lt.limit_with_vat,0) *
               CASE WHEN lt.unit LIKE '%/год'   THEN 0.25
                    WHEN lt.unit LIKE '%/месяц' THEN 3
                    ELSE 1 END AS amount_q3,
             COALESCE(fl.qty_q4,0) * COALESCE(lt.limit_with_vat,0) *
               CASE WHEN lt.unit LIKE '%/год'   THEN 0.25
                    WHEN lt.unit LIKE '%/месяц' THEN 3
                    ELSE 1 END AS amount_q4
      FROM v3.limit_template lt
      LEFT JOIN v3.form_limit fl
        ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id
      WHERE lt.row_type = 'LEAF'
    ),
    agg AS (
      SELECT t.root_id,
        SUM(la.qty_q1)    AS qty_q1,    SUM(la.amount_q1) AS amount_q1,
        SUM(la.qty_q2)    AS qty_q2,    SUM(la.amount_q2) AS amount_q2,
        SUM(la.qty_q3)    AS qty_q3,    SUM(la.amount_q3) AS amount_q3,
        SUM(la.qty_q4)    AS qty_q4,    SUM(la.amount_q4) AS amount_q4
      FROM tree t
      JOIN leaf_amt la ON la.id = t.desc_id
      GROUP BY t.root_id
    )
    SELECT
      -- Маппинг физического хранения limit_template → DTO:
      --   SECTION → ROOT, GROUP → GROUP, LEAF → INPUT (редактируемая ячейка).
      CASE lt.row_type WHEN 'SECTION' THEN 'ROOT'
                       WHEN 'GROUP'   THEN 'GROUP'
                       WHEN 'LEAF'    THEN 'INPUT' END::VARCHAR,
      CASE lt.row_type WHEN 'SECTION' THEN 0 WHEN 'GROUP' THEN 1 ELSE 2 END,
      lt.sort_order::BIGINT,
      jsonb_build_object(
        'id',               lt.id,             -- LEAF id для адресации в write API (= template_id)
        'section_no',       lt.section_no,
        'expense_item_code',lt.expense_item_code,
        'name',             lt.name,
        'unit',             lt.unit,
        'limit_with_vat',   lt.limit_with_vat,
        'limit_without_vat',lt.limit_without_vat,
        'qty_q1',           a.qty_q1,    'amount_q1', a.amount_q1,
        'qty_q2',           a.qty_q2,    'amount_q2', a.amount_q2,
        'qty_q3',           a.qty_q3,    'amount_q3', a.amount_q3,
        'qty_q4',           a.qty_q4,    'amount_q4', a.amount_q4,
        'comment',          fl.comment
      )
    FROM v3.limit_template lt
    LEFT JOIN agg a ON a.root_id = lt.id
    LEFT JOIN v3.form_limit fl
      ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id
    ORDER BY lt.sort_order;
    RETURN;
  END IF;

  -- ═══ АХР_аренда ════════════════════════════════════════════════════════
  IF p_sheet = 'AHR_RENT' THEN
    RETURN QUERY
    SELECT
      'INPUT'::VARCHAR,
      0,
      ROW_NUMBER() OVER (ORDER BY rd.id) AS sort_order,
      jsonb_build_object(
        'id',               rd.id,         -- id rent_detail (для write API)
        'line_id',          rd.line_id,
        'vsp_id',           rd.vsp_id,
        'address',          v.address,
        'object_type',      v.format,
        'rented_area',      v.total_area,
        'contract_number',  rd.contract_number,
        'contract_end_date',rd.contract_end_date,
        'plan',             jsonb_build_object('q1', rd.plan_q1, 'q2', rd.plan_q2, 'q3', rd.plan_q3, 'q4', rd.plan_q4,
                                               'year', COALESCE(rd.plan_q1,0)+COALESCE(rd.plan_q2,0)+COALESCE(rd.plan_q3,0)+COALESCE(rd.plan_q4,0)),
        'fact_q1',          jsonb_build_object('jan', rd.actual_jan, 'feb', rd.actual_feb, 'mar', rd.actual_mar,
                                               'total', COALESCE(rd.actual_jan,0)+COALESCE(rd.actual_feb,0)+COALESCE(rd.actual_mar,0)),
        'fact_q2',          jsonb_build_object('apr', rd.actual_apr, 'may', rd.actual_may, 'jun', rd.actual_jun,
                                               'total', COALESCE(rd.actual_apr,0)+COALESCE(rd.actual_may,0)+COALESCE(rd.actual_jun,0)),
        'fact_q3',          jsonb_build_object('jul', rd.actual_jul, 'aug', rd.actual_aug, 'sep', rd.actual_sep,
                                               'total', COALESCE(rd.actual_jul,0)+COALESCE(rd.actual_aug,0)+COALESCE(rd.actual_sep,0)),
        'fact_q4',          jsonb_build_object('oct', rd.actual_oct, 'nov', rd.actual_nov, 'dec', rd.actual_dec,
                                               'total', COALESCE(rd.actual_oct,0)+COALESCE(rd.actual_nov,0)+COALESCE(rd.actual_dec,0)),
        'comment',          rd.comment
      )
    FROM v3.rent_detail rd
    JOIN v3.budget_line bl ON bl.id = rd.line_id
    JOIN v3.vsp v          ON v.id  = rd.vsp_id
    WHERE bl.budget_form_id = p_form_id
    ORDER BY rd.id;
    RETURN;
  END IF;

  -- ═══ АХР_коммунал ═══════════════════════════════════════════════════════
  IF p_sheet = 'AHR_UTILITY' THEN
    RETURN QUERY
    SELECT
      'INPUT'::VARCHAR,
      0,
      ROW_NUMBER() OVER (ORDER BY ud.id) AS sort_order,
      jsonb_build_object(
        'id',               ud.id,         -- id utility_detail (для write API)
        'line_id',          ud.line_id,
        'vsp_id',           ud.vsp_id,
        'address',          v.address,
        'object_type',      v.format,
        'object_area',      v.total_area,
        'contract_number',  ud.contract_number,
        'contract_end_date',ud.contract_end_date,
        'plan',             jsonb_build_object('q1', ud.plan_q1, 'q2', ud.plan_q2, 'q3', ud.plan_q3, 'q4', ud.plan_q4,
                                               'year', COALESCE(ud.plan_q1,0)+COALESCE(ud.plan_q2,0)+COALESCE(ud.plan_q3,0)+COALESCE(ud.plan_q4,0)),
        'fact_q1',          jsonb_build_object('jan', ud.actual_jan, 'feb', ud.actual_feb, 'mar', ud.actual_mar,
                                               'total', COALESCE(ud.actual_jan,0)+COALESCE(ud.actual_feb,0)+COALESCE(ud.actual_mar,0)),
        'fact_q2',          jsonb_build_object('apr', ud.actual_apr, 'may', ud.actual_may, 'jun', ud.actual_jun,
                                               'total', COALESCE(ud.actual_apr,0)+COALESCE(ud.actual_may,0)+COALESCE(ud.actual_jun,0)),
        'fact_q3',          jsonb_build_object('jul', ud.actual_jul, 'aug', ud.actual_aug, 'sep', ud.actual_sep,
                                               'total', COALESCE(ud.actual_jul,0)+COALESCE(ud.actual_aug,0)+COALESCE(ud.actual_sep,0)),
        'fact_q4',          jsonb_build_object('oct', ud.actual_oct, 'nov', ud.actual_nov, 'dec', ud.actual_dec,
                                               'total', COALESCE(ud.actual_oct,0)+COALESCE(ud.actual_nov,0)+COALESCE(ud.actual_dec,0)),
        'comment',          ud.comment
      )
    FROM v3.utility_detail ud
    JOIN v3.budget_line bl ON bl.id = ud.line_id
    JOIN v3.vsp v          ON v.id  = ud.vsp_id
    WHERE bl.budget_form_id = p_form_id
    ORDER BY ud.id;
    RETURN;
  END IF;

  -- ═══ АХР_охрана ═════════════════════════════════════════════════════════
  IF p_sheet = 'AHR_SECURITY' THEN
    RETURN QUERY
    SELECT
      'INPUT'::VARCHAR,
      0,
      ROW_NUMBER() OVER (ORDER BY sd.id) AS sort_order,
      jsonb_build_object(
        'id',               sd.id,         -- id security_detail (для write API)
        'line_id',          sd.line_id,
        'vsp_id',           sd.vsp_id,
        'address',          v.address,
        'object_type',      v.format,
        'object_area',      v.total_area,
        'contract_number',  sd.contract_number,
        'contract_end_date',sd.contract_end_date,
        'plan',             jsonb_build_object('q1', sd.plan_q1, 'q2', sd.plan_q2, 'q3', sd.plan_q3, 'q4', sd.plan_q4,
                                               'year', COALESCE(sd.plan_q1,0)+COALESCE(sd.plan_q2,0)+COALESCE(sd.plan_q3,0)+COALESCE(sd.plan_q4,0)),
        'fact_q1',          jsonb_build_object('jan', sd.actual_jan, 'feb', sd.actual_feb, 'mar', sd.actual_mar,
                                               'total', COALESCE(sd.actual_jan,0)+COALESCE(sd.actual_feb,0)+COALESCE(sd.actual_mar,0)),
        'fact_q2',          jsonb_build_object('apr', sd.actual_apr, 'may', sd.actual_may, 'jun', sd.actual_jun,
                                               'total', COALESCE(sd.actual_apr,0)+COALESCE(sd.actual_may,0)+COALESCE(sd.actual_jun,0)),
        'fact_q3',          jsonb_build_object('jul', sd.actual_jul, 'aug', sd.actual_aug, 'sep', sd.actual_sep,
                                               'total', COALESCE(sd.actual_jul,0)+COALESCE(sd.actual_aug,0)+COALESCE(sd.actual_sep,0)),
        'fact_q4',          jsonb_build_object('oct', sd.actual_oct, 'nov', sd.actual_nov, 'dec', sd.actual_dec,
                                               'total', COALESCE(sd.actual_oct,0)+COALESCE(sd.actual_nov,0)+COALESCE(sd.actual_dec,0)),
        'comment',          sd.comment
      )
    FROM v3.security_detail sd
    JOIN v3.budget_line bl ON bl.id = sd.line_id
    JOIN v3.vsp v          ON v.id  = sd.vsp_id
    WHERE bl.budget_form_id = p_form_id
    ORDER BY sd.id;
    RETURN;
  END IF;

  -- ═══ Отч.9ф (Fixed Asset Report) ════════════════════════════════════════
  -- Одна строка = (expense_item × equipment_name × month).
  --
  -- opening_qty/amt_604/60415 хранятся в БД ТОЛЬКО для month=1 (база года).
  -- Для month=2..12 вычисляются на лету: накопительная сумма дельт по
  -- предыдущим месяцам от base (Excel: opening_M2 = F1+H1−J1).
  --
  -- totals.* — также computed (acquired_total, disposed_total, balance_qty/amt).
  IF p_sheet = 'OTCH9F' THEN
    RETURN QUERY
    WITH base AS (
      SELECT far.id, far.budget_form_id, far.expense_item_id,
             far.equipment_name, far.month,
             far.acquired_qty_604,    far.acquired_amt_604,
             far.disposed_qty_604,    far.disposed_amt_604,
             far.acquired_qty_60415,  far.acquired_amt_60415,
             far.transferred_qty_60415, far.transferred_amt_60415,
             far.go_balance_only_amt,
             -- база: opening_qty/amt из month=1 этой группы
             FIRST_VALUE(COALESCE(far.opening_qty_604,0))
               OVER w AS base_qty_604,
             FIRST_VALUE(COALESCE(far.opening_amt_604,0))
               OVER w AS base_amt_604,
             FIRST_VALUE(COALESCE(far.opening_qty_60415,0))
               OVER w AS base_qty_60415,
             FIRST_VALUE(COALESCE(far.opening_amt_60415,0))
               OVER w AS base_amt_60415,
             -- кумулятивная дельта по предыдущим месяцам в группе
             COALESCE(SUM(COALESCE(far.acquired_qty_604,0) - COALESCE(far.disposed_qty_604,0))
               OVER w_prev, 0) AS cum_dq_604,
             COALESCE(SUM(COALESCE(far.acquired_amt_604,0) - COALESCE(far.disposed_amt_604,0))
               OVER w_prev, 0) AS cum_da_604,
             COALESCE(SUM(COALESCE(far.acquired_qty_60415,0) - COALESCE(far.transferred_qty_60415,0))
               OVER w_prev, 0) AS cum_dq_60415,
             COALESCE(SUM(COALESCE(far.acquired_amt_60415,0) - COALESCE(far.transferred_amt_60415,0))
               OVER w_prev, 0) AS cum_da_60415
      FROM v3.fixed_asset_report far
      WHERE far.budget_form_id = p_form_id
      WINDOW w AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name
                   ORDER BY far.month),
             w_prev AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name
                        ORDER BY far.month
                        ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)
    ),
    eff AS (
      SELECT b.*,
             (b.base_qty_604 + b.cum_dq_604)     AS opening_qty_604_eff,
             (b.base_amt_604 + b.cum_da_604)     AS opening_amt_604_eff,
             (b.base_qty_60415 + b.cum_dq_60415) AS opening_qty_60415_eff,
             (b.base_amt_60415 + b.cum_da_60415) AS opening_amt_60415_eff
      FROM base b
    )
    SELECT
      'INPUT'::VARCHAR,
      0,
      ROW_NUMBER() OVER (ORDER BY eff.expense_item_id, eff.month) AS sort_order,
      jsonb_build_object(
        'id',               eff.id,        -- fixed_asset_report.id (для write API)
        'expense_item_id',  ei.id,
        'item_id',          ei.item_id,
        'section_code',     ei.section_code,
        'equipment_name',   eff.equipment_name,
        'month',            eff.month,
        'b_604', jsonb_build_object(
          'opening_qty',    eff.opening_qty_604_eff,
          'opening_amt',    eff.opening_amt_604_eff,
          'acquired_qty',   eff.acquired_qty_604,
          'acquired_amt',   eff.acquired_amt_604,
          'disposed_qty',   eff.disposed_qty_604,
          'disposed_amt',   eff.disposed_amt_604
        ),
        'b_60415', jsonb_build_object(
          'opening_qty',    eff.opening_qty_60415_eff,
          'opening_amt',    eff.opening_amt_60415_eff,
          'acquired_qty',   eff.acquired_qty_60415,
          'acquired_amt',   eff.acquired_amt_60415,
          'transferred_qty',eff.transferred_qty_60415,
          'transferred_amt',eff.transferred_amt_60415
        ),
        'totals', jsonb_build_object(
          -- Excel R = SUM(I, O, S) − Q
          'acquired_total', COALESCE(eff.acquired_amt_604,0)+COALESCE(eff.acquired_amt_60415,0)
                            +COALESCE(eff.go_balance_only_amt,0)-COALESCE(eff.transferred_amt_60415,0),
          'go_balance_only',eff.go_balance_only_amt,
          -- Excel T = K
          'disposed_total', COALESCE(eff.disposed_amt_604,0),
          -- Excel U = (opening+acquired−disposed)_604 + (opening+acquired−transferred)_60415
          'balance_qty',    (eff.opening_qty_604_eff   + COALESCE(eff.acquired_qty_604,0)   - COALESCE(eff.disposed_qty_604,0))
                          + (eff.opening_qty_60415_eff + COALESCE(eff.acquired_qty_60415,0) - COALESCE(eff.transferred_qty_60415,0)),
          'balance_amt',    (eff.opening_amt_604_eff   + COALESCE(eff.acquired_amt_604,0)   - COALESCE(eff.disposed_amt_604,0))
                          + (eff.opening_amt_60415_eff + COALESCE(eff.acquired_amt_60415,0) - COALESCE(eff.transferred_amt_60415,0))
        )
      )
    FROM eff
    JOIN v3.expense_item ei ON ei.id = eff.expense_item_id
    ORDER BY eff.expense_item_id, eff.month;
    RETURN;
  END IF;

  -- ═══ Смета_2026 ═════════════════════════════════════════════════════════
  IF p_sheet = 'SMETA' THEN
    RETURN QUERY
    SELECT
      sm.row_type,
      sm.depth,
      ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order,
      jsonb_build_object(
        'section_code', sm.section_code,
        'name',         sm.name,
        'plan', jsonb_build_object(
          'support',     jsonb_build_object('q1', sm.plan_supp_q1, 'q2', sm.plan_supp_q2, 'q3', sm.plan_supp_q3, 'q4', sm.plan_supp_q4, 'year', sm.plan_supp_year),
          'development', jsonb_build_object('q1', sm.plan_dev_q1,  'q2', sm.plan_dev_q2,  'q3', sm.plan_dev_q3,  'q4', sm.plan_dev_q4,  'year', sm.plan_dev_year)
        ),
        'approved', jsonb_build_object(
          'support',     jsonb_build_object('q1', sm.appr_supp_q1, 'q2', sm.appr_supp_q2, 'q3', sm.appr_supp_q3, 'q4', sm.appr_supp_q4, 'year', sm.appr_supp_year),
          'development', jsonb_build_object('q1', sm.appr_dev_q1,  'q2', sm.appr_dev_q2,  'q3', sm.appr_dev_q3,  'q4', sm.appr_dev_q4,  'year', sm.appr_dev_year)
        ),
        'fact', jsonb_build_object(
          'support',     jsonb_build_object('q1', sm.fact_supp_q1, 'q2', sm.fact_supp_q2, 'q3', sm.fact_supp_q3, 'q4', sm.fact_supp_q4, 'year', sm.fact_supp_year),
          'development', jsonb_build_object('q1', sm.fact_dev_q1,  'q2', sm.fact_dev_q2,  'q3', sm.fact_dev_q3,  'q4', sm.fact_dev_q4,  'year', sm.fact_dev_year)
        ),
        -- corrected: только Q2/Q3/Q4 (см. шаблон Смета_2026, блок V-X). Q1 нет
        -- (Q1 corrected ≡ approved_q1), year нет (q1+q2+q3+q4 даёт двойной счёт переносов).
        'corrected', jsonb_build_object(
          'support',     jsonb_build_object('q2', sm.corr_supp_q2, 'q3', sm.corr_supp_q3, 'q4', sm.corr_supp_q4),
          'development', jsonb_build_object('q2', sm.corr_dev_q2,  'q3', sm.corr_dev_q3,  'q4', sm.corr_dev_q4)
        )
      )
    FROM v3.v_form2_smeta(p_form_id) sm
    ORDER BY sm.section_code;
    RETURN;
  END IF;

  RAISE EXCEPTION 'Unknown sheet: %', p_sheet
    USING HINT = 'Use AHR / CAP / OPER / AHR_LIMIT / AHR_RENT / AHR_UTILITY / AHR_SECURITY / OTCH9F / SMETA';

END;
$function$
;

-- DROP FUNCTION v3.v_form3_report_sections(int4, _text);

CREATE OR REPLACE FUNCTION v3.v_form3_report_sections(p_report_id integer, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_q1_adj_by_items numeric, col_q1_adj_increase numeric, col_q1_total_corr numeric, col_q1_m1 numeric, col_q1_m2 numeric, col_q1_m3 numeric, col_q1_quarter_actual numeric, col_q1_economy numeric, col_q2_carryover numeric, col_q2_adj_by_items numeric, col_q2_adj_increase numeric, col_q2_total_corr numeric, col_q2_m1 numeric, col_q2_m2 numeric, col_q2_m3 numeric, col_q2_quarter_actual numeric, col_q2_economy numeric, col_q3_carryover numeric, col_q3_adj_by_items numeric, col_q3_adj_increase numeric, col_q3_total_corr numeric, col_q3_m1 numeric, col_q3_m2 numeric, col_q3_m3 numeric, col_q3_quarter_actual numeric, col_q3_economy numeric, col_q4_carryover numeric, col_q4_adj_by_items numeric, col_q4_adj_increase numeric, col_q4_total_corr numeric, col_q4_m1 numeric, col_q4_m2 numeric, col_q4_m3 numeric, col_q4_spod numeric, col_q4_quarter_actual numeric, col_q4_economy numeric, col_year_total_corr numeric, col_year_total_actual numeric, col_year_economy numeric, _sort_path integer[])
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    s_q1   BOOLEAN := p_sections IS NULL OR 'q1'   = ANY(p_sections);
    s_q2   BOOLEAN := p_sections IS NULL OR 'q2'   = ANY(p_sections);
    s_q3   BOOLEAN := p_sections IS NULL OR 'q3'   = ANY(p_sections);
    s_q4   BOOLEAN := p_sections IS NULL OR 'q4'   = ANY(p_sections);
    s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections);
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  RETURN QUERY
  WITH
    -- Дерево expense_item, ограниченное FORM_3 (через junction)
    tw AS (
      SELECT t.* FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft
        ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3'
    ),
    -- INPUT строки: одна на v3.rf_project_report_line
    page AS (
      SELECT l.id AS lid, l.expense_item_id AS eid,
             t.parent_item_id AS sc,        -- section-код родителя в дереве (1.01.1.)
             ei.item_id AS ic,
             ei.num_group_id AS ng, ei.name AS ename,
             t.path AS tree_path
      FROM v3.rf_project_report_line l
      JOIN v3.expense_item ei ON ei.id = l.expense_item_id
      JOIN tw t ON t.id = l.expense_item_id
      WHERE l.rf_project_report_id = p_report_id
    ),
    -- Поквартальные данные
    jq AS (
      SELECT q.rf_project_report_line_id AS lid, q.quarter,
             q.adj_by_items, q.adj_increase,
             q.actual_m1, q.actual_m2, q.actual_m3, q.actual_spod
      FROM v3.rf_project_report_quarter q
      JOIN v3.rf_project_report_line l ON l.id = q.rf_project_report_line_id
      WHERE l.rf_project_report_id = p_report_id
    ),
    -- INPUT base: соединяем page + 4 квартала
    input_base AS (
      SELECT
        pg.lid, pg.eid, pg.sc, pg.ic, pg.ng, pg.ename, pg.tree_path,
        q1.adj_by_items AS q1_abi, q1.adj_increase AS q1_ai,
        q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3,
        q2.adj_by_items AS q2_abi, q2.adj_increase AS q2_ai,
        q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3,
        q3.adj_by_items AS q3_abi, q3.adj_increase AS q3_ai,
        q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3,
        q4.adj_by_items AS q4_abi, q4.adj_increase AS q4_ai,
        q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3,
        q4.actual_spod AS q4_spod
      FROM page pg
      LEFT JOIN jq q1 ON q1.lid = pg.lid AND q1.quarter = 1
      LEFT JOIN jq q2 ON q2.lid = pg.lid AND q2.quarter = 2
      LEFT JOIN jq q3 ON q3.lid = pg.lid AND q3.quarter = 3
      LEFT JOIN jq q4 ON q4.lid = pg.lid AND q4.quarter = 4
    ),
    -- Computed totals per row
    enriched AS (
      SELECT b.*,
        COALESCE(b.q1_abi,0) + COALESCE(b.q1_ai,0) AS q1_tc,
        COALESCE(b.q1_m1,0) + COALESCE(b.q1_m2,0) + COALESCE(b.q1_m3,0) AS q1_qa,
        COALESCE(b.q2_abi,0) + COALESCE(b.q2_ai,0) AS q2_tc,
        COALESCE(b.q2_m1,0) + COALESCE(b.q2_m2,0) + COALESCE(b.q2_m3,0) AS q2_qa,
        COALESCE(b.q3_abi,0) + COALESCE(b.q3_ai,0) AS q3_tc,
        COALESCE(b.q3_m1,0) + COALESCE(b.q3_m2,0) + COALESCE(b.q3_m3,0) AS q3_qa,
        COALESCE(b.q4_abi,0) + COALESCE(b.q4_ai,0) AS q4_tc,
        COALESCE(b.q4_m1,0) + COALESCE(b.q4_m2,0) + COALESCE(b.q4_m3,0) + COALESCE(b.q4_spod,0) AS q4_qa
      FROM input_base b
    ),
    -- Per-line aggregates for tree rollup
    agg_q AS (
      SELECT l.expense_item_id AS eid, q.quarter,
        SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai,
        SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3,
        SUM(q.actual_spod) AS spod
      FROM v3.rf_project_report_line l
      JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id
      WHERE l.rf_project_report_id = p_report_id
      GROUP BY l.expense_item_id, q.quarter
    ),
    tw_aq1 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 1
      GROUP BY tw.id
    ),
    tw_aq2 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 2
      GROUP BY tw.id
    ),
    tw_aq3 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 3
      GROUP BY tw.id
    ),
    tw_aq4 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.spod) AS spod
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 4
      GROUP BY tw.id
    )

  -- ═══ Часть A: INPUT строки ════════════════════════════════════════════════
  SELECT * FROM (
    SELECT
      'INPUT'::VARCHAR AS row_type, 3 AS depth, e.lid::INT AS line_id,
      e.sc, e.ic, e.ng, e.ename,
      -- Q1
      CASE WHEN s_q1 THEN e.q1_abi END,
      CASE WHEN s_q1 THEN e.q1_ai END,
      CASE WHEN s_q1 THEN e.q1_tc END,
      CASE WHEN s_q1 THEN e.q1_m1 END,
      CASE WHEN s_q1 THEN e.q1_m2 END,
      CASE WHEN s_q1 THEN e.q1_m3 END,
      CASE WHEN s_q1 THEN e.q1_qa END,
      CASE WHEN s_q1 THEN ROUND(e.q1_tc - e.q1_qa, 1) END,
      -- Q2 (carryover пока NULL — нет столбца в _quarter; задел на будущее)
      CASE WHEN s_q2 THEN NULL::NUMERIC END,
      CASE WHEN s_q2 THEN e.q2_abi END,
      CASE WHEN s_q2 THEN e.q2_ai END,
      CASE WHEN s_q2 THEN e.q2_tc END,
      CASE WHEN s_q2 THEN e.q2_m1 END,
      CASE WHEN s_q2 THEN e.q2_m2 END,
      CASE WHEN s_q2 THEN e.q2_m3 END,
      CASE WHEN s_q2 THEN e.q2_qa END,
      CASE WHEN s_q2 THEN ROUND(e.q2_tc - e.q2_qa, 1) END,
      -- Q3
      CASE WHEN s_q3 THEN NULL::NUMERIC END,
      CASE WHEN s_q3 THEN e.q3_abi END,
      CASE WHEN s_q3 THEN e.q3_ai END,
      CASE WHEN s_q3 THEN e.q3_tc END,
      CASE WHEN s_q3 THEN e.q3_m1 END,
      CASE WHEN s_q3 THEN e.q3_m2 END,
      CASE WHEN s_q3 THEN e.q3_m3 END,
      CASE WHEN s_q3 THEN e.q3_qa END,
      CASE WHEN s_q3 THEN ROUND(e.q3_tc - e.q3_qa, 1) END,
      -- Q4
      CASE WHEN s_q4 THEN NULL::NUMERIC END,
      CASE WHEN s_q4 THEN e.q4_abi END,
      CASE WHEN s_q4 THEN e.q4_ai END,
      CASE WHEN s_q4 THEN e.q4_tc END,
      CASE WHEN s_q4 THEN e.q4_m1 END,
      CASE WHEN s_q4 THEN e.q4_m2 END,
      CASE WHEN s_q4 THEN e.q4_m3 END,
      CASE WHEN s_q4 THEN e.q4_spod END,
      CASE WHEN s_q4 THEN e.q4_qa END,
      CASE WHEN s_q4 THEN ROUND(e.q4_tc - e.q4_qa, 1) END,
      -- year
      CASE WHEN s_year THEN e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc END,
      CASE WHEN s_year THEN e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa END,
      CASE WHEN s_year THEN ROUND((e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc) -
                                  (e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa), 1) END,
      e.tree_path || ARRAY[e.lid::INT] AS _sort_path
    FROM enriched e

    UNION ALL

    -- ═══ Часть B: Иерархия (ROOT / GROUP / ITEM / SUB_ITEM) ═════════════════
    SELECT
      CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP'
                   WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
      t.depth, NULL::INT,
      CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
      CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END,
      t.num_group_id, t.name,
      -- Q1
      CASE WHEN s_q1 THEN a1.abi END,
      CASE WHEN s_q1 THEN a1.ai END,
      CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END,
      CASE WHEN s_q1 THEN a1.m1 END,
      CASE WHEN s_q1 THEN a1.m2 END,
      CASE WHEN s_q1 THEN a1.m3 END,
      CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END,
      CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0)) -
                                (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END,
      -- Q2
      CASE WHEN s_q2 THEN NULL::NUMERIC END,
      CASE WHEN s_q2 THEN a2.abi END,
      CASE WHEN s_q2 THEN a2.ai END,
      CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END,
      CASE WHEN s_q2 THEN a2.m1 END,
      CASE WHEN s_q2 THEN a2.m2 END,
      CASE WHEN s_q2 THEN a2.m3 END,
      CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END,
      CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0)) -
                                (COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END,
      -- Q3
      CASE WHEN s_q3 THEN NULL::NUMERIC END,
      CASE WHEN s_q3 THEN a3.abi END,
      CASE WHEN s_q3 THEN a3.ai END,
      CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END,
      CASE WHEN s_q3 THEN a3.m1 END,
      CASE WHEN s_q3 THEN a3.m2 END,
      CASE WHEN s_q3 THEN a3.m3 END,
      CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END,
      CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0)) -
                                (COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END,
      -- Q4
      CASE WHEN s_q4 THEN NULL::NUMERIC END,
      CASE WHEN s_q4 THEN a4.abi END,
      CASE WHEN s_q4 THEN a4.ai END,
      CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
      CASE WHEN s_q4 THEN a4.m1 END,
      CASE WHEN s_q4 THEN a4.m2 END,
      CASE WHEN s_q4 THEN a4.m3 END,
      CASE WHEN s_q4 THEN a4.spod END,
      CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
      CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) -
                                (COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END,
      -- year
      CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+
                            COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
                            COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+
                            COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
      CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
                            COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
                            COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
                            COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
      CASE WHEN s_year THEN
        ROUND(
          (COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
           COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0))
          -
          (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
           COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
           COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
           COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)),
          1)
      END,
      t.path
    FROM tw t
    LEFT JOIN tw_aq1 a1 ON a1.id = t.id
    LEFT JOIN tw_aq2 a2 ON a2.id = t.id
    LEFT JOIN tw_aq3 a3 ON a3.id = t.id
    LEFT JOIN tw_aq4 a4 ON a4.id = t.id
  ) sub
  ORDER BY sub._sort_path;

END;
$function$
;

-- DROP FUNCTION v3.v_form3_report_jsonb(int4, _text);

CREATE OR REPLACE FUNCTION v3.v_form3_report_jsonb(p_report_id integer, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE sql
 STABLE
AS $function$
  SELECT
    s.row_type,
    s.depth,
    ROW_NUMBER() OVER (ORDER BY s._sort_path) AS sort_order,
    jsonb_build_object(
      'line_id', s.line_id,
      'header', jsonb_build_object(
        'section_code',    s.col_section_code,
        'item_id',         s.col_item_id,
        'num_group_id',    s.col_num_group_id,
        'name',            s.col_name,
        -- expense_item_id из _sort_path: для INPUT путь = {...ei_id, line_id},
        -- для иерархии = {...ei_id} → последний/предпоследний элемент.
        'expense_item_id', CASE WHEN s.row_type = 'INPUT'
                             THEN s._sort_path[array_upper(s._sort_path,1)-1]
                             ELSE s._sort_path[array_upper(s._sort_path,1)] END
      ),
      'q1', jsonb_build_object(
        'adj_by_items',   s.col_q1_adj_by_items,
        'adj_increase',   s.col_q1_adj_increase,
        'total_corr',     s.col_q1_total_corr,
        'm1',             s.col_q1_m1,
        'm2',             s.col_q1_m2,
        'm3',             s.col_q1_m3,
        'quarter_actual', s.col_q1_quarter_actual,
        'economy',        s.col_q1_economy
      ),
      'q2', jsonb_build_object(
        'carryover',      s.col_q2_carryover,
        'adj_by_items',   s.col_q2_adj_by_items,
        'adj_increase',   s.col_q2_adj_increase,
        'total_corr',     s.col_q2_total_corr,
        'm1',             s.col_q2_m1,
        'm2',             s.col_q2_m2,
        'm3',             s.col_q2_m3,
        'quarter_actual', s.col_q2_quarter_actual,
        'economy',        s.col_q2_economy
      ),
      'q3', jsonb_build_object(
        'carryover',      s.col_q3_carryover,
        'adj_by_items',   s.col_q3_adj_by_items,
        'adj_increase',   s.col_q3_adj_increase,
        'total_corr',     s.col_q3_total_corr,
        'm1',             s.col_q3_m1,
        'm2',             s.col_q3_m2,
        'm3',             s.col_q3_m3,
        'quarter_actual', s.col_q3_quarter_actual,
        'economy',        s.col_q3_economy
      ),
      'q4', jsonb_build_object(
        'carryover',      s.col_q4_carryover,
        'adj_by_items',   s.col_q4_adj_by_items,
        'adj_increase',   s.col_q4_adj_increase,
        'total_corr',     s.col_q4_total_corr,
        'm1',             s.col_q4_m1,
        'm2',             s.col_q4_m2,
        'm3',             s.col_q4_m3,
        'spod',           s.col_q4_spod,
        'quarter_actual', s.col_q4_quarter_actual,
        'economy',        s.col_q4_economy
      ),
      'totals', jsonb_build_object(
        'total_corr',   s.col_year_total_corr,
        'total_actual', s.col_year_total_actual,
        'economy',      s.col_year_economy
      )
    ) AS data
  FROM v3.v_form3_report_sections(p_report_id, p_sections) s
  ORDER BY s._sort_path;
$function$
;

-- DROP FUNCTION v3.v_form3_rf_rollup_sections(int4, int4, _text);

CREATE OR REPLACE FUNCTION v3.v_form3_rf_rollup_sections(p_branch_id integer, p_year integer, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_q1_adj_by_items numeric, col_q1_adj_increase numeric, col_q1_total_corr numeric, col_q1_m1 numeric, col_q1_m2 numeric, col_q1_m3 numeric, col_q1_quarter_actual numeric, col_q1_economy numeric, col_q2_adj_by_items numeric, col_q2_adj_increase numeric, col_q2_total_corr numeric, col_q2_m1 numeric, col_q2_m2 numeric, col_q2_m3 numeric, col_q2_quarter_actual numeric, col_q2_economy numeric, col_q3_adj_by_items numeric, col_q3_adj_increase numeric, col_q3_total_corr numeric, col_q3_m1 numeric, col_q3_m2 numeric, col_q3_m3 numeric, col_q3_quarter_actual numeric, col_q3_economy numeric, col_q4_adj_by_items numeric, col_q4_adj_increase numeric, col_q4_total_corr numeric, col_q4_m1 numeric, col_q4_m2 numeric, col_q4_m3 numeric, col_q4_spod numeric, col_q4_quarter_actual numeric, col_q4_economy numeric, col_year_total_corr numeric, col_year_total_actual numeric, col_year_economy numeric, _sort_path integer[])
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    s_q1   BOOLEAN := p_sections IS NULL OR 'q1'   = ANY(p_sections);
    s_q2   BOOLEAN := p_sections IS NULL OR 'q2'   = ANY(p_sections);
    s_q3   BOOLEAN := p_sections IS NULL OR 'q3'   = ANY(p_sections);
    s_q4   BOOLEAN := p_sections IS NULL OR 'q4'   = ANY(p_sections);
    s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections);
BEGIN
  RETURN QUERY
  WITH
    tw AS (
      SELECT t.* FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft
        ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3'
    ),
    -- Агрегаты per (expense_item_id, quarter) — sum по обоим трекам всех проектов РФ × год
    agg_q AS (
      SELECT l.expense_item_id AS eid, q.quarter,
        SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai,
        SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3,
        SUM(q.actual_spod) AS spod
      FROM v3.rf_project_report r
      JOIN v3.project p                    ON p.id = r.project_id
      JOIN v3.rf_project_report_line l     ON l.rf_project_report_id = r.id
      JOIN v3.rf_project_report_quarter q  ON q.rf_project_report_line_id = l.id
      WHERE p.org_unit_id = p_branch_id AND r.year = p_year
      GROUP BY l.expense_item_id, q.quarter
    ),
    tw_aq1 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
                      SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
               FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=1
               GROUP BY tw.id),
    tw_aq2 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
                      SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
               FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=2
               GROUP BY tw.id),
    tw_aq3 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
                      SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
               FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=3
               GROUP BY tw.id),
    tw_aq4 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
                      SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
                      SUM(a.spod) AS spod
               FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=4
               GROUP BY tw.id)

  SELECT
    CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP'
                 WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
    t.depth,
    CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
    CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END,
    t.num_group_id, t.name,
    -- Q1
    CASE WHEN s_q1 THEN a1.abi END,
    CASE WHEN s_q1 THEN a1.ai END,
    CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END,
    CASE WHEN s_q1 THEN a1.m1 END,
    CASE WHEN s_q1 THEN a1.m2 END,
    CASE WHEN s_q1 THEN a1.m3 END,
    CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END,
    CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0))
                            -(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END,
    -- Q2
    CASE WHEN s_q2 THEN a2.abi END,
    CASE WHEN s_q2 THEN a2.ai END,
    CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END,
    CASE WHEN s_q2 THEN a2.m1 END,
    CASE WHEN s_q2 THEN a2.m2 END,
    CASE WHEN s_q2 THEN a2.m3 END,
    CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END,
    CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0))
                            -(COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END,
    -- Q3
    CASE WHEN s_q3 THEN a3.abi END,
    CASE WHEN s_q3 THEN a3.ai END,
    CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END,
    CASE WHEN s_q3 THEN a3.m1 END,
    CASE WHEN s_q3 THEN a3.m2 END,
    CASE WHEN s_q3 THEN a3.m3 END,
    CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END,
    CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0))
                            -(COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END,
    -- Q4
    CASE WHEN s_q4 THEN a4.abi END,
    CASE WHEN s_q4 THEN a4.ai END,
    CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
    CASE WHEN s_q4 THEN a4.m1 END,
    CASE WHEN s_q4 THEN a4.m2 END,
    CASE WHEN s_q4 THEN a4.m3 END,
    CASE WHEN s_q4 THEN a4.spod END,
    CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
    CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0))
                            -(COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END,
    -- year
    CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
                          COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
    CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
                          COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
                          COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
                          COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
    CASE WHEN s_year THEN
      ROUND(
        (COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
         COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0))
       -(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
         COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
         COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
         COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)),
        1)
    END,
    t.path
  FROM tw t
  LEFT JOIN tw_aq1 a1 ON a1.id = t.id
  LEFT JOIN tw_aq2 a2 ON a2.id = t.id
  LEFT JOIN tw_aq3 a3 ON a3.id = t.id
  LEFT JOIN tw_aq4 a4 ON a4.id = t.id
  ORDER BY t.path;
END;
$function$
;

-- DROP FUNCTION v3.v_form3_rf_rollup_jsonb(int4, int4, _text);

CREATE OR REPLACE FUNCTION v3.v_form3_rf_rollup_jsonb(p_branch_id integer, p_year integer, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE sql
 STABLE
AS $function$
  SELECT
    s.row_type,
    s.depth,
    ROW_NUMBER() OVER (ORDER BY s._sort_path) AS sort_order,
    jsonb_build_object(
      'header', jsonb_build_object(
        'section_code', s.col_section_code,
        'item_id',      s.col_item_id,
        'num_group_id', s.col_num_group_id,
        'name',         s.col_name
      ),
      'q1', jsonb_build_object(
        'adj_by_items',   s.col_q1_adj_by_items,
        'adj_increase',   s.col_q1_adj_increase,
        'total_corr',     s.col_q1_total_corr,
        'm1',             s.col_q1_m1,
        'm2',             s.col_q1_m2,
        'm3',             s.col_q1_m3,
        'quarter_actual', s.col_q1_quarter_actual,
        'economy',        s.col_q1_economy
      ),
      'q2', jsonb_build_object(
        'adj_by_items',   s.col_q2_adj_by_items,
        'adj_increase',   s.col_q2_adj_increase,
        'total_corr',     s.col_q2_total_corr,
        'm1',             s.col_q2_m1,
        'm2',             s.col_q2_m2,
        'm3',             s.col_q2_m3,
        'quarter_actual', s.col_q2_quarter_actual,
        'economy',        s.col_q2_economy
      ),
      'q3', jsonb_build_object(
        'adj_by_items',   s.col_q3_adj_by_items,
        'adj_increase',   s.col_q3_adj_increase,
        'total_corr',     s.col_q3_total_corr,
        'm1',             s.col_q3_m1,
        'm2',             s.col_q3_m2,
        'm3',             s.col_q3_m3,
        'quarter_actual', s.col_q3_quarter_actual,
        'economy',        s.col_q3_economy
      ),
      'q4', jsonb_build_object(
        'adj_by_items',   s.col_q4_adj_by_items,
        'adj_increase',   s.col_q4_adj_increase,
        'total_corr',     s.col_q4_total_corr,
        'm1',             s.col_q4_m1,
        'm2',             s.col_q4_m2,
        'm3',             s.col_q4_m3,
        'spod',           s.col_q4_spod,
        'quarter_actual', s.col_q4_quarter_actual,
        'economy',        s.col_q4_economy
      ),
      'totals', jsonb_build_object(
        'total_corr',   s.col_year_total_corr,
        'total_actual', s.col_year_total_actual,
        'economy',      s.col_year_economy
      )
    ) AS data
  FROM v3.v_form3_rf_rollup_sections(p_branch_id, p_year, p_sections) s
  ORDER BY s._sort_path;
$function$
;

-- DROP FUNCTION v3.v_form3_smeta(int4, int4);

CREATE OR REPLACE FUNCTION v3.v_form3_smeta(p_branch_id integer, p_year integer)
 RETURNS TABLE(row_type character varying, section_code character varying, name character varying, total_corr numeric, total_actual numeric, economy numeric)
 LANGUAGE sql
 STABLE
AS $function$
  -- Top-level sections (ROOT) + общая итоговая
  SELECT row_type, col_section_code, col_name,
         col_year_total_corr, col_year_total_actual, col_year_economy
  FROM v3.v_form3_rf_rollup_sections(p_branch_id, p_year, ARRAY['year'])
  WHERE depth = 0

  UNION ALL

  -- Итог: «Всего расходов по смете (п.1+п.3)»
  SELECT 'TOTAL'::VARCHAR, NULL::VARCHAR,
         'Всего расходов по смете (п.1+п.3)'::VARCHAR,
         SUM(col_year_total_corr),
         SUM(col_year_total_actual),
         ROUND(SUM(col_year_total_corr) - SUM(col_year_total_actual), 1)
  FROM v3.v_form3_rf_rollup_sections(p_branch_id, p_year, ARRAY['year'])
  WHERE depth = 0;
$function$
;

-- DROP FUNCTION v3.v_form4_sheet_sections(int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form4_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_justification character varying, col_internal_order character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cs_amount numeric, col_cs_reference character varying, col_cs_counterparty character varying, col_cs_deadline character varying, col_cs_comment character varying, col_cs_future_y1 numeric, col_cs_future_y2 numeric, col_cs_other_ssp numeric, col_cs_centralized_flag character varying, col_al_contract_ref character varying, col_al_purpose character varying, col_rsv_q1 numeric, col_rsv_q2 numeric, col_rsv_q3 numeric, col_rsv_q4 numeric, col_rsv_year numeric, col_rsv_just character varying, col_col_amount numeric, col_col_protocol character varying, col_col_note character varying, col_ckk_ceiling numeric, col_ckk_q1 numeric, col_ckk_q2 numeric, col_ckk_q3 numeric, col_ckk_q4 numeric, col_ckk_rf_schedule character varying, col_ckk_delivery_deadline character varying, col_ckk_procurement_plan character varying, col_ckk_procurement_method character varying, col_ckk_comment character varying, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_addenda character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_q1 numeric, col_cd_q2 numeric, col_cd_q3 numeric, col_cd_q4 numeric, col_cd_rf_schedule character varying, col_cd_vat_rate character varying, col_cd_exchange_rate numeric, col_cd_amount_foreign numeric, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_rf numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_ho numeric, col_q1_pay_rf numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_rf numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_ho numeric, col_q2_pay_rf numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_rf numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_ho numeric, col_q3_pay_rf numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_rf numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_ho numeric, col_q4_pay_rf numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, _sort_path integer[])
 LANGUAGE plpgsql
 STABLE
AS $function$
#variable_conflict use_column
DECLARE
  s_plan BOOL; s_seq_d BOOL; s_appr BOOL;
  s_cd BOOL; s_book BOOL;
  s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL;
  s_cs BOOL; s_al BOOL; s_rsv BOOL; s_col BOOL; s_ckk BOOL;
  s_need_ap BOOL;  -- plan/seq/reserve нужны для approved + cp1..cp4
BEGIN
  s_plan  := p_sections IS NULL OR 'plan'             = ANY(p_sections);
  s_seq_d := p_sections IS NULL OR 'seq_dfip'         = ANY(p_sections);
  s_appr  := p_sections IS NULL OR 'approved'         = ANY(p_sections);
  s_cd    := p_sections IS NULL OR 'contract'         = ANY(p_sections);
  s_book  := p_sections IS NULL OR 'booking'          = ANY(p_sections);
  s_q1    := p_sections IS NULL OR 'q1'               = ANY(p_sections);
  s_q2    := p_sections IS NULL OR 'q2'               = ANY(p_sections);
  s_q3    := p_sections IS NULL OR 'q3'               = ANY(p_sections);
  s_q4    := p_sections IS NULL OR 'q4'               = ANY(p_sections);
  s_tot   := p_sections IS NULL OR 'totals'           = ANY(p_sections);
  s_cs    := p_sections IS NULL OR 'contract_summary' = ANY(p_sections);
  s_al    := p_sections IS NULL OR 'allocation'       = ANY(p_sections);
  s_rsv   := p_sections IS NULL OR 'reserve'          = ANY(p_sections);
  s_col   := p_sections IS NULL OR 'collegial'        = ANY(p_sections);
  s_ckk   := p_sections IS NULL OR 'ckk'              = ANY(p_sections);
  s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4;

  RETURN QUERY
  WITH
    tw AS (
      SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name,
             t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids
      FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id
      JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code
      WHERE t.sheet = p_sheet
    ),
    page AS (
      SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.name AS bname, bl.vsp_id
      FROM v3.budget_line bl
      JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
      WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
    ),
    -- Conditional joins (gated by section flags)
    jp   AS (SELECT p.* FROM v3.plan p JOIN page pg ON p.line_id = pg.lid WHERE s_need_ap),
    jsd  AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='DFIP'),
    jr   AS (SELECT r.* FROM v3.reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_need_ap),
    jcd  AS (SELECT c.* FROM v3.contract_detail c JOIN page pg ON c.line_id = pg.lid WHERE s_cd),
    jck  AS (SELECT c.* FROM v3.ckk c JOIN page pg ON c.line_id = pg.lid WHERE s_book OR s_ckk),
    jcs  AS (SELECT c.* FROM v3.contract_summary c JOIN page pg ON c.line_id = pg.lid WHERE s_cs),
    jal  AS (SELECT a.* FROM v3.allocation a JOIN page pg ON a.line_id = pg.lid WHERE s_al),
    jcol AS (SELECT c.* FROM v3.collegial_approval c JOIN page pg ON c.line_id = pg.lid WHERE s_col),
    jq1  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q1 AND q.quarter=1),
    jq2  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q2 AND q.quarter=2),
    jq3  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q3 AND q.quarter=3),
    jq4  AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE (s_q4 OR s_tot) AND q.quarter=4),

    -- INPUT строки: enriched данные
    input_base AS (
      SELECT
        pg.lid, pg.eid, pg.bname, pg.vsp_id,
        v.address AS vsp_addr,
        bl_just.justification AS just, bl_just.internal_order AS io,
        t.parent_item_id AS sc, t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, t.path AS tree_path,
        -- plan
        COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2,
        COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4,
        p.comment AS pcmt,
        -- seq dfip / ssp_go
        COALESCE(sd.adj_q1,0) AS dq1, COALESCE(sd.adj_q2,0) AS dq2,
        COALESCE(sd.adj_q3,0) AS dq3, COALESCE(sd.adj_q4,0) AS dq4,
        sd.justification AS djust,
        -- reserve
        COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2,
        COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4,
        -- contract (расширено: q1-4, addenda, exchange_rate, amount_foreign)
        cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.addenda AS cd_add,
        cd.contract_date AS cd_dt, cd.subject AS cd_subj, cd.currency AS cd_cur,
        cd.ceiling_amount AS cd_ceil,
        cd.expenses_q1 AS cd_q1, cd.expenses_q2 AS cd_q2, cd.expenses_q3 AS cd_q3, cd.expenses_q4 AS cd_q4,
        cd.rf_schedule AS cd_rfsch,
        cd.vat_rate AS cd_vat, cd.exchange_rate AS cd_xr, cd.amount_foreign AS cd_af,
        cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch,
        cd.act AS cd_act, cd.comment AS cd_cmt,
        -- ckk (booking из expenses_q* + полный блок: ceiling, rf_schedule, …)
        ck.expenses_q1 AS bk1, ck.expenses_q2 AS bk2, ck.expenses_q3 AS bk3, ck.expenses_q4 AS bk4,
        ck.expenses_next_year_q1 AS bn1, ck.expenses_next_year_q2 AS bn2,
        ck.expenses_next_year_q3 AS bn3, ck.expenses_next_year_q4 AS bn4,
        ck.ceiling_amount AS ck_ceil, ck.rf_schedule AS ck_rfsch,
        ck.delivery_deadline AS ck_dd, ck.procurement_plan AS ck_pp,
        ck.procurement_method AS ck_pm, ck.comment AS ck_cmt,
        -- contract_summary (Действующий договор)
        cs.total_amount AS cs_amt, cs.reference AS cs_ref, cs.counterparty AS cs_cp,
        cs.deadline AS cs_dl, cs.comment AS cs_cmt,
        cs.future_payments_y1 AS cs_y1, cs.future_payments_y2 AS cs_y2,
        cs.other_ssp_amount AS cs_oss, cs.centralized_flag AS cs_cflag,
        -- allocation
        al.contract_ref AS al_ref, al.allocation_purpose AS al_purp,
        -- collegial
        col.approved_amount AS col_amt, col.protocol_reference AS col_pr, col.note AS col_note,
        -- Q1
        q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_rf AS q1_arf, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt,
        q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa,
        q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, q1.payment_comment AS q1_pcmt,
        q1.payment_act AS q1_pact, q1.booking_amount AS q1_book,
        q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3,
        q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4,
        q1.transfer_to_economy AS q1_te,
        q1.transfer_delay_acts AS q1_tda, q1.transfer_delay_procurement AS q1_tdp,
        q1.transfer_economy_rf AS q1_terf,
        q1.transfer_next_comment AS q1_tnc, q1.transfer_far_comment AS q1_tfc,
        -- Q2
        q2.target_change AS q2_tc, q2.base_plan_correction AS q2_bc, q2.base_plan_correction_comment AS q2_bcc,
        q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq,
        q2.plan_revision_comment AS q2_rcmt,
        q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_rf AS q2_arf, q2.adj_reserve AS q2_arv,
        q2.adj_comment AS q2_acmt,
        q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa,
        q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf,
        q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact,
        q2.booking_amount AS q2_book,
        q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3,
        q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, q2.transfer_to_economy AS q2_te,
        q2.transfer_delay_acts AS q2_tda, q2.transfer_delay_procurement AS q2_tdp,
        q2.transfer_economy_rf AS q2_terf,
        q2.transfer_next_comment AS q2_tnc, q2.transfer_far_comment AS q2_tfc,
        -- Q3
        q3.target_change AS q3_tc, q3.base_plan_correction AS q3_bc,
        q3.base_plan_correction_comment AS q3_bcc,
        q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq,
        q3.plan_revision_comment AS q3_rcmt,
        q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_rf AS q3_arf, q3.adj_reserve AS q3_arv,
        q3.adj_comment AS q3_acmt,
        q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa,
        q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf,
        q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact,
        q3.booking_amount AS q3_book,
        q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3,
        q3.transfer_to_q4 AS q3_tq4, q3.transfer_to_economy AS q3_te,
        q3.transfer_delay_acts AS q3_tda, q3.transfer_delay_procurement AS q3_tdp,
        q3.transfer_economy_rf AS q3_terf, q3.transfer_next_comment AS q3_tnc,
        -- Q4
        q4.target_change AS q4_tc, q4.base_plan_correction AS q4_bc,
        q4.base_plan_correction_comment AS q4_bcc,
        q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq,
        q4.plan_revision_comment AS q4_rcmt,
        q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_rf AS q4_arf, q4.adj_reserve AS q4_arv,
        q4.adj_comment AS q4_acmt,
        q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa,
        q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf,
        q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact,
        q4.booking_amount AS q4_book,
        q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod,
        q4.transfer_to_economy AS q4_te,
        -- approved per quarter
        COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1,
        COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2,
        COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3,
        COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4
      FROM page pg
      JOIN tw t ON t.id = pg.eid
      LEFT JOIN v3.vsp v ON v.id = pg.vsp_id
      LEFT JOIN v3.budget_line bl_just ON bl_just.id = pg.lid
      LEFT JOIN jp p   ON p.line_id  = pg.lid
      LEFT JOIN jsd sd ON sd.line_id = pg.lid
      LEFT JOIN jr r   ON r.line_id  = pg.lid
      LEFT JOIN jcd cd ON cd.line_id = pg.lid
      LEFT JOIN jck ck ON ck.line_id = pg.lid
      LEFT JOIN jcs cs ON cs.line_id = pg.lid
      LEFT JOIN jal al ON al.line_id = pg.lid
      LEFT JOIN jcol col ON col.line_id = pg.lid
      LEFT JOIN jq1 q1 ON q1.line_id = pg.lid
      LEFT JOIN jq2 q2 ON q2.line_id = pg.lid
      LEFT JOIN jq3 q3 ON q3.line_id = pg.lid
      LEFT JOIN jq4 q4 ON q4.line_id = pg.lid
    ),
    -- Computed: corrected/new plans
    -- Excel: CH10 (cp1) = AM10 + SUM(CB:CE) − CF10
    --        DJ10 (np2) = SUM(AN10, CY10, DH10, CF10, DE10:DG10)
    -- В Excel CY и CF — две отдельные колонки (CY = «Закрытие 1-го квартала / Перенос
    -- во 2 кв», CF = плановый «Перенос во 2 кв» внутри корректировок 1 кв). В DB
    -- хранится одно поле q1.transfer_to_q2 — оно работает «за двоих»: вычитается
    -- из cp1 (как CF) и прибавляется к np2 (как CY). Допущение: CY = CF.
    enriched AS (
      SELECT b.*,
        -- Q1 corrected_plan = approved + adj_* − transfer_to_q2
        b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_arf,0)+COALESCE(b.q1_arv,0)
              - COALESCE(b.q1_tq2,0) AS cp1,
        COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1,
        -- Q2 new_plan = approved + transfer_q1 + revision_inc/seq + base_correction + target_change
        b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) + COALESCE(b.q2_bc,0)+COALESCE(b.q2_tc,0) AS np2,
        COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2,
        b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) + COALESCE(b.q3_bc,0)+COALESCE(b.q3_tc,0) AS np3,
        COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3,
        b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) + COALESCE(b.q4_bc,0)+COALESCE(b.q4_tc,0) AS np4,
        COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4
      FROM input_base b
    ),
    final_input AS (
      SELECT e.*,
        -- Q2 cp = new_plan + adj_* − transfer_to_q3
        e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_arf,0)+COALESCE(e.q2_arv,0)
              - COALESCE(e.q2_tq3,0) AS cp2,
        -- Q3 cp = new_plan + adj_* − transfer_to_q4
        e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_arf,0)+COALESCE(e.q3_arv,0)
              - COALESCE(e.q3_tq4,0) AS cp3,
        -- Q4 cp = new_plan + adj_* (нет дальнейшего переноса)
        e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_arf,0)+COALESCE(e.q4_arv,0) AS cp4
      FROM enriched e
    ),

    -- Aggregates per expense_item (для иерархии)
    agg AS (
      SELECT bl.expense_item_id AS eid,
        SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4,
        SUM(sd.adj_q1) AS sd1, SUM(sd.adj_q2) AS sd2, SUM(sd.adj_q3) AS sd3, SUM(sd.adj_q4) AS sd4,
        SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4,
        SUM(ck.expenses_q1) AS bk1, SUM(ck.expenses_q2) AS bk2, SUM(ck.expenses_q3) AS bk3, SUM(ck.expenses_q4) AS bk4,
        SUM(ck.expenses_next_year_q1) AS bn1, SUM(ck.expenses_next_year_q2) AS bn2,
        SUM(ck.expenses_next_year_q3) AS bn3, SUM(ck.expenses_next_year_q4) AS bn4
      FROM v3.budget_line bl
      JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
      LEFT JOIN v3.plan p ON p.line_id = bl.id AND s_need_ap
      LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor='DFIP' AND s_need_ap
      LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap
      LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book
      WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
        AND (s_need_ap OR s_book)
      GROUP BY bl.expense_item_id
    ),
    -- Per-quarter aggregates from blq
    aq_q AS (
      SELECT bl.expense_item_id AS eid, q.quarter,
        SUM(q.adj_current) AS ac, SUM(q.adj_ssp) AS as_v, SUM(q.adj_rf) AS arf, SUM(q.adj_reserve) AS arv,
        SUM(q.payment_amount) AS pa,
        SUM(q.payment_amount_ho) AS pho, SUM(q.payment_amount_rf) AS prf,
        SUM(q.booking_amount) AS book,
        SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3,
        SUM(q.actual_spod) AS spod,
        SUM(q.transfer_to_q2) AS tq2, SUM(q.transfer_to_q3) AS tq3, SUM(q.transfer_to_q4) AS tq4,
        SUM(q.transfer_to_economy) AS te,
        -- Δ к approved для перехода к new_plan: revision_inc/seq + target_change + base_correction
        -- (Excel: DJ10 = SUM(AN, CY, DH, CF, DE:DG)). Должно совпадать с INPUT-формулой np.
        SUM(COALESCE(q.plan_revision_increase,0)
          + COALESCE(q.plan_revision_sequester,0)
          + COALESCE(q.target_change,0)
          + COALESCE(q.base_plan_correction,0)) AS rev
      FROM v3.budget_line bl
      JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
      JOIN v3.budget_line_quarter q ON q.line_id = bl.id
      WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
      GROUP BY bl.expense_item_id, q.quarter
    ),
    -- Tree-rollup
    tw_agg AS (
      SELECT tw.id,
        SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4,
        SUM(a.sd1) AS sd1, SUM(a.sd2) AS sd2, SUM(a.sd3) AS sd3, SUM(a.sd4) AS sd4,
        SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4,
        SUM(a.bk1) AS bk1, SUM(a.bk2) AS bk2, SUM(a.bk3) AS bk3, SUM(a.bk4) AS bk4,
        SUM(a.bn1) AS bn1, SUM(a.bn2) AS bn2, SUM(a.bn3) AS bn3, SUM(a.bn4) AS bn4
      FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids)
      GROUP BY tw.id
    ),
    tw_aq1 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.book) AS book,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
        SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=1 GROUP BY tw.id
    ),
    tw_aq2 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.book) AS book,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
        SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=2 GROUP BY tw.id
    ),
    tw_aq3 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.book) AS book,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3,
        SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=3 GROUP BY tw.id
    ),
    tw_aq4 AS (
      SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv,
        SUM(b.pa) AS pa, SUM(b.book) AS book,
        SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod,
        SUM(b.te) AS te, SUM(b.rev) AS rev
      FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=4 GROUP BY tw.id
    )

  -- ═══ Часть A: INPUT строки ═════════════════════════════════════════════
  SELECT * FROM (
    SELECT
      'INPUT'::VARCHAR, 3, f.lid::INT,
      f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename),
      f.just, f.io, f.vsp_id, f.vsp_addr,
      -- plan
      CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END,
      CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END,
      CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END,
      -- seq dfip
      CASE WHEN s_seq_d THEN f.dq1 END, CASE WHEN s_seq_d THEN f.dq2 END,
      CASE WHEN s_seq_d THEN f.dq3 END, CASE WHEN s_seq_d THEN f.dq4 END,
      CASE WHEN s_seq_d THEN f.dq1+f.dq2+f.dq3+f.dq4 END, CASE WHEN s_seq_d THEN f.djust END,
      -- approved
      CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END,
      CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END,
      CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END,
      -- contract_summary (Действующий договор)
      CASE WHEN s_cs THEN f.cs_amt END, CASE WHEN s_cs THEN f.cs_ref END,
      CASE WHEN s_cs THEN f.cs_cp END, CASE WHEN s_cs THEN f.cs_dl END,
      CASE WHEN s_cs THEN f.cs_cmt END,
      CASE WHEN s_cs THEN f.cs_y1 END, CASE WHEN s_cs THEN f.cs_y2 END,
      CASE WHEN s_cs THEN f.cs_oss END, CASE WHEN s_cs THEN f.cs_cflag END,
      -- allocation
      CASE WHEN s_al THEN f.al_ref END, CASE WHEN s_al THEN f.al_purp END,
      -- reserve (отдельный блок)
      CASE WHEN s_rsv THEN f.rq1 END, CASE WHEN s_rsv THEN f.rq2 END,
      CASE WHEN s_rsv THEN f.rq3 END, CASE WHEN s_rsv THEN f.rq4 END,
      CASE WHEN s_rsv THEN f.rq1+f.rq2+f.rq3+f.rq4 END, NULL::VARCHAR,
      -- collegial
      CASE WHEN s_col THEN f.col_amt END, CASE WHEN s_col THEN f.col_pr END,
      CASE WHEN s_col THEN f.col_note END,
      -- ckk полный
      CASE WHEN s_ckk THEN f.ck_ceil END,
      CASE WHEN s_ckk THEN f.bk1 END, CASE WHEN s_ckk THEN f.bk2 END,
      CASE WHEN s_ckk THEN f.bk3 END, CASE WHEN s_ckk THEN f.bk4 END,
      CASE WHEN s_ckk THEN f.ck_rfsch END, CASE WHEN s_ckk THEN f.ck_dd END,
      CASE WHEN s_ckk THEN f.ck_pp END, CASE WHEN s_ckk THEN f.ck_pm END,
      CASE WHEN s_ckk THEN f.ck_cmt END,
      -- contract (расширено)
      CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END,
      CASE WHEN s_cd THEN f.cd_add END,
      CASE WHEN s_cd THEN f.cd_dt END, CASE WHEN s_cd THEN f.cd_subj END,
      CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END,
      CASE WHEN s_cd THEN f.cd_q1 END, CASE WHEN s_cd THEN f.cd_q2 END,
      CASE WHEN s_cd THEN f.cd_q3 END, CASE WHEN s_cd THEN f.cd_q4 END,
      CASE WHEN s_cd THEN f.cd_rfsch END,
      CASE WHEN s_cd THEN f.cd_vat END,
      CASE WHEN s_cd THEN f.cd_xr END, CASE WHEN s_cd THEN f.cd_af END,
      CASE WHEN s_cd THEN f.cd_dl END,
      CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END,
      CASE WHEN s_cd THEN f.cd_cmt END,
      -- booking 2026
      CASE WHEN s_book THEN f.bk1 END, CASE WHEN s_book THEN f.bk2 END,
      CASE WHEN s_book THEN f.bk3 END, CASE WHEN s_book THEN f.bk4 END,
      -- booking 2027
      CASE WHEN s_book THEN f.bn1 END, CASE WHEN s_book THEN f.bn2 END,
      CASE WHEN s_book THEN f.bn3 END, CASE WHEN s_book THEN f.bn4 END,
      -- Q1
      CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END,
      CASE WHEN s_q1 THEN f.q1_arf END,
      CASE WHEN s_q1 THEN f.q1_arv END,
      CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END,
      CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END,
      CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, CASE WHEN s_q1 THEN f.q1_pcmt END,
      CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN f.q1_book END,
      CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END,
      CASE WHEN s_q1 THEN f.aq1 END,
      CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.q1_book,0) END,    -- residual_after_booking
      CASE WHEN s_q1 THEN f.cp1 - f.aq1 END,                    -- residual_after_actual
      CASE WHEN s_q1 THEN f.q1_tq2 END,
      CASE WHEN s_q1 THEN f.q1_tda END, CASE WHEN s_q1 THEN f.q1_tdp END, CASE WHEN s_q1 THEN f.q1_terf END,
      CASE WHEN s_q1 THEN f.q1_tnc END,
      CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END,
      CASE WHEN s_q1 THEN f.q1_tfc END,
      CASE WHEN s_q1 THEN f.q1_te END,
      -- total = "Закрытие квартала" = сумма transfer-колонок
      CASE WHEN s_q1 THEN COALESCE(f.q1_tq2,0)+COALESCE(f.q1_tq3,0)+COALESCE(f.q1_tq4,0)+COALESCE(f.q1_te,0) END,
      -- Q2
      CASE WHEN s_q2 THEN f.q2_tc END, CASE WHEN s_q2 THEN f.q2_bc END, CASE WHEN s_q2 THEN f.q2_bcc END,
      CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END,
      CASE WHEN s_q2 THEN f.q2_rcmt END,
      CASE WHEN s_q2 THEN f.np2 END,
      CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END,
      CASE WHEN s_q2 THEN f.q2_arf END,
      CASE WHEN s_q2 THEN f.q2_arv END,
      CASE WHEN s_q2 THEN f.q2_acmt END,
      CASE WHEN s_q2 THEN f.cp2 END,
      CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END,
      CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END,
      CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END,
      CASE WHEN s_q2 THEN f.q2_book END,
      CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END,
      CASE WHEN s_q2 THEN f.aq2 END,
      CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.q2_book,0) END,
      CASE WHEN s_q2 THEN f.cp2 - f.aq2 END,
      CASE WHEN s_q2 THEN f.q2_tq3 END,
      CASE WHEN s_q2 THEN f.q2_tda END, CASE WHEN s_q2 THEN f.q2_tdp END, CASE WHEN s_q2 THEN f.q2_terf END,
      CASE WHEN s_q2 THEN f.q2_tnc END,
      CASE WHEN s_q2 THEN f.q2_tq4 END,
      CASE WHEN s_q2 THEN f.q2_tfc END,
      CASE WHEN s_q2 THEN f.q2_te END,
      CASE WHEN s_q2 THEN COALESCE(f.q2_tq3,0)+COALESCE(f.q2_tq4,0)+COALESCE(f.q2_te,0) END,
      -- Q3
      CASE WHEN s_q3 THEN f.q3_tc END, CASE WHEN s_q3 THEN f.q3_bc END,
      CASE WHEN s_q3 THEN f.q3_bcc END,
      CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END,
      CASE WHEN s_q3 THEN f.q3_rcmt END,
      CASE WHEN s_q3 THEN f.np3 END,
      CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END,
      CASE WHEN s_q3 THEN f.q3_arf END,
      CASE WHEN s_q3 THEN f.q3_arv END,
      CASE WHEN s_q3 THEN f.q3_acmt END,
      CASE WHEN s_q3 THEN f.cp3 END,
      CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END,
      CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END,
      CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END,
      CASE WHEN s_q3 THEN f.q3_book END,
      CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END,
      CASE WHEN s_q3 THEN f.aq3 END,
      CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.q3_book,0) END,
      CASE WHEN s_q3 THEN f.cp3 - f.aq3 END,
      CASE WHEN s_q3 THEN f.q3_tq4 END,
      CASE WHEN s_q3 THEN f.q3_tda END, CASE WHEN s_q3 THEN f.q3_tdp END, CASE WHEN s_q3 THEN f.q3_terf END,
      CASE WHEN s_q3 THEN f.q3_tnc END,
      CASE WHEN s_q3 THEN f.q3_te END,
      CASE WHEN s_q3 THEN COALESCE(f.q3_tq4,0)+COALESCE(f.q3_te,0) END,
      -- Q4
      CASE WHEN s_q4 THEN f.q4_tc END, CASE WHEN s_q4 THEN f.q4_bc END,
      CASE WHEN s_q4 THEN f.q4_bcc END,
      CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END,
      CASE WHEN s_q4 THEN f.q4_rcmt END,
      CASE WHEN s_q4 THEN f.np4 END,
      CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END,
      CASE WHEN s_q4 THEN f.q4_arf END,
      CASE WHEN s_q4 THEN f.q4_arv END,
      CASE WHEN s_q4 THEN f.q4_acmt END,
      CASE WHEN s_q4 THEN f.cp4 END,
      CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END,
      CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END,
      CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END,
      CASE WHEN s_q4 THEN f.q4_book END,
      CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END,
      CASE WHEN s_q4 THEN f.q4_spod END,
      CASE WHEN s_q4 THEN f.aq4 END,
      CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.q4_book,0) END,
      CASE WHEN s_q4 THEN f.cp4 - f.aq4 END,
      CASE WHEN s_q4 THEN f.q4_te END,
      CASE WHEN s_q4 THEN COALESCE(f.q4_te,0) END,
      -- totals
      CASE WHEN s_tot THEN f.aq1+f.aq2+f.aq3+f.aq4 END,        -- fact_year

      f.tree_path || ARRAY[f.lid::INT] AS _sort
    FROM final_input f

    UNION ALL

    -- ═══ Часть B: Иерархия ════════════════════════════════════════════════
    SELECT
      CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
      t.depth, NULL::INT,
      CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
      CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END,
      t.num_group_id, t.name,
      NULL::VARCHAR, NULL::VARCHAR,                          -- justification, internal_order
      NULL::INT, NULL::VARCHAR,
      -- plan
      CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END,
      CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END,
      CASE WHEN s_plan THEN COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) END,
      NULL::VARCHAR,
      -- seq dfip
      CASE WHEN s_seq_d THEN ta.sd1 END, CASE WHEN s_seq_d THEN ta.sd2 END,
      CASE WHEN s_seq_d THEN ta.sd3 END, CASE WHEN s_seq_d THEN ta.sd4 END,
      CASE WHEN s_seq_d THEN COALESCE(ta.sd1,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sd4,0) END,
      NULL::VARCHAR,
      -- approved
      CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) END,
      CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0)
                          +COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0)
                          +COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0)
                          +COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) END,
      -- contract_summary (NULL на иерархии — текстовые)
      NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
      NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,
      -- allocation
      NULL::VARCHAR, NULL::VARCHAR,
      -- reserve (q1-4 + year агрегаты, justification NULL)
      CASE WHEN s_rsv THEN ta.sr1 END, CASE WHEN s_rsv THEN ta.sr2 END,
      CASE WHEN s_rsv THEN ta.sr3 END, CASE WHEN s_rsv THEN ta.sr4 END,
      CASE WHEN s_rsv THEN COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0) END,
      NULL::VARCHAR,
      -- collegial
      NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR,
      -- ckk полный (q1-4 агрегаты есть в ta.bk*, остальное NULL)
      NULL::NUMERIC,
      CASE WHEN s_ckk THEN ta.bk1 END, CASE WHEN s_ckk THEN ta.bk2 END,
      CASE WHEN s_ckk THEN ta.bk3 END, CASE WHEN s_ckk THEN ta.bk4 END,
      NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
      -- contract (расширено) — все NULL на иерархии
      NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
      NULL::DATE, NULL::VARCHAR,
      NULL::VARCHAR, NULL::NUMERIC,
      NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
      NULL::VARCHAR,
      NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC,
      NULL::VARCHAR,
      NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR,
      -- booking 2026 / 2027
      CASE WHEN s_book THEN ta.bk1 END, CASE WHEN s_book THEN ta.bk2 END,
      CASE WHEN s_book THEN ta.bk3 END, CASE WHEN s_book THEN ta.bk4 END,
      CASE WHEN s_book THEN ta.bn1 END, CASE WHEN s_book THEN ta.bn2 END,
      CASE WHEN s_book THEN ta.bn3 END, CASE WHEN s_book THEN ta.bn4 END,
      -- Q1 hierarchy
      CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as_v END,
      CASE WHEN s_q1 THEN tb1.arf END,
      CASE WHEN s_q1 THEN tb1.arv END,
      NULL::VARCHAR,
      -- corrected_plan = approved + adj_* − transfer_to_q2
      CASE WHEN s_q1 THEN
        COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0)
        +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0)
        -COALESCE(tb1.tq2,0)
      END,
      NULL::DATE,
      CASE WHEN s_q1 THEN tb1.pa END,
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,
      NULL::VARCHAR,                                             -- pay_act
      CASE WHEN s_q1 THEN tb1.book END,                          -- booking агрегат
      CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END,
      CASE WHEN s_q1 THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) END,
      -- residual_after_booking = cp − booking
      CASE WHEN s_q1 THEN
        (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0)
         +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0)
         -COALESCE(tb1.tq2,0))
        - COALESCE(tb1.book,0)
      END,
      -- residual_after_actual = cp − actual
      CASE WHEN s_q1 THEN
        (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0)
         +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0)
         -COALESCE(tb1.tq2,0))
        - (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0))
      END,
      CASE WHEN s_q1 THEN tb1.tq2 END,
      NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,  -- tq2 reasons + next comment
      CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END,
      NULL::VARCHAR,                                             -- far comment
      CASE WHEN s_q1 THEN tb1.te END,
      -- total = "Закрытие квартала" = сумма transfer-колонок
      CASE WHEN s_q1 THEN
        COALESCE(tb1.tq2,0)+COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)+COALESCE(tb1.te,0)
      END,
      -- Q2 hierarchy
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,    -- target_change/base_correction
      NULL::NUMERIC, NULL::NUMERIC,                   -- revision_inc/revision_seq
      NULL::VARCHAR,                                  -- revision_comment
      CASE WHEN s_q2 THEN
        COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)
      END,
      CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as_v END,
      CASE WHEN s_q2 THEN tb2.arf END,
      CASE WHEN s_q2 THEN tb2.arv END,
      NULL::VARCHAR,                                  -- adj_comment
      -- corrected_plan = new_plan + adj_* − transfer_to_q3
      CASE WHEN s_q2 THEN
        COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0)
        +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)
        +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0)
        -COALESCE(tb2.tq3,0)
      END,
      NULL::DATE,
      CASE WHEN s_q2 THEN tb2.pa END,
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,    -- pay_ho/pay_rf/pay_comment
      NULL::VARCHAR,                                  -- pay_act
      CASE WHEN s_q2 THEN tb2.book END,
      CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END,
      CASE WHEN s_q2 THEN COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) END,
      -- residual_after_booking = cp − booking
      CASE WHEN s_q2 THEN
        (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0)
         +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)
         +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0)
         -COALESCE(tb2.tq3,0))
        - COALESCE(tb2.book,0)
      END,
      -- residual_after_actual = cp − actual
      CASE WHEN s_q2 THEN
        (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0)
         +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)
         +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0)
         -COALESCE(tb2.tq3,0))
        - (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0))
      END,
      CASE WHEN s_q2 THEN tb2.tq3 END,
      NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,  -- tq3 reasons + next comment
      CASE WHEN s_q2 THEN tb2.tq4 END,
      NULL::VARCHAR,                                  -- far comment
      CASE WHEN s_q2 THEN tb2.te END,
      -- total = сумма transfer-колонок q2
      CASE WHEN s_q2 THEN
        COALESCE(tb2.tq3,0)+COALESCE(tb2.tq4,0)+COALESCE(tb2.te,0)
      END,
      -- Q3 hierarchy
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,    -- target_change/base_correction/comment
      NULL::NUMERIC, NULL::NUMERIC,                   -- revision_inc/revision_seq
      NULL::VARCHAR,
      CASE WHEN s_q3 THEN
        COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)
      END,
      CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as_v END,
      CASE WHEN s_q3 THEN tb3.arf END,
      CASE WHEN s_q3 THEN tb3.arv END,
      NULL::VARCHAR,
      -- corrected_plan = new_plan + adj_* − transfer_to_q4
      CASE WHEN s_q3 THEN
        COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0)
        +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)
        +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0)
        -COALESCE(tb3.tq4,0)
      END,
      NULL::DATE,
      CASE WHEN s_q3 THEN tb3.pa END,
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,
      NULL::VARCHAR,
      CASE WHEN s_q3 THEN tb3.book END,
      CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END,
      CASE WHEN s_q3 THEN COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) END,
      -- residual_after_booking = cp − booking
      CASE WHEN s_q3 THEN
        (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0)
         +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)
         +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0)
         -COALESCE(tb3.tq4,0))
        - COALESCE(tb3.book,0)
      END,
      -- residual_after_actual = cp − actual
      CASE WHEN s_q3 THEN
        (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0)
         +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)
         +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0)
         -COALESCE(tb3.tq4,0))
        - (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0))
      END,
      CASE WHEN s_q3 THEN tb3.tq4 END,
      NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,
      CASE WHEN s_q3 THEN tb3.te END,
      -- total = сумма transfer-колонок q3
      CASE WHEN s_q3 THEN
        COALESCE(tb3.tq4,0)+COALESCE(tb3.te,0)
      END,
      -- Q4 hierarchy
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,    -- target_change/base_correction/comment
      NULL::NUMERIC, NULL::NUMERIC,                   -- revision_inc/revision_seq
      NULL::VARCHAR,
      CASE WHEN s_q4 THEN
        COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)
      END,
      CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as_v END,
      CASE WHEN s_q4 THEN tb4.arf END,
      CASE WHEN s_q4 THEN tb4.arv END,
      NULL::VARCHAR,
      -- corrected_plan = new_plan + adj_* (нет дальнейшего переноса)
      CASE WHEN s_q4 THEN
        COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0)
        +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)
        +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0)
      END,
      NULL::DATE,
      CASE WHEN s_q4 THEN tb4.pa END,
      NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR,
      NULL::VARCHAR,
      CASE WHEN s_q4 THEN tb4.book END,
      CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END,
      CASE WHEN s_q4 THEN tb4.spod END,
      CASE WHEN s_q4 THEN COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END,
      -- residual_after_booking = cp − booking
      CASE WHEN s_q4 THEN
        (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0)
         +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)
         +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0))
        - COALESCE(tb4.book,0)
      END,
      -- residual_after_actual = cp − actual
      CASE WHEN s_q4 THEN
        (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0)
         +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)
         +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0))
        - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0))
      END,
      CASE WHEN s_q4 THEN tb4.te END,
      -- total = "Закрытие" q4 = transfer_to_economy
      CASE WHEN s_q4 THEN COALESCE(tb4.te,0) END,
      -- totals
      CASE WHEN s_tot THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)
                          +COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)
                          +COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)
                          +COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END,  -- fact_year

      t.path AS _sort
    FROM tw t
    LEFT JOIN tw_agg ta ON ta.id = t.id
    LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id
    LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id
    LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id
    LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id
  ) sub
  ORDER BY sub._sort;
END;
$function$
;

-- DROP FUNCTION v3.v_form4_sheet_lines_jsonb(int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form4_sheet_lines_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, approved_data jsonb, contract_summary_data jsonb, allocation_data jsonb, reserve_data jsonb, collegial_data jsonb, ckk_data jsonb, contract_data jsonb, booking_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[])
 LANGUAGE sql
 STABLE
AS $function$
  WITH flags AS (
    SELECT
      (p_sections IS NULL OR 'plan'     = ANY(p_sections)) AS s_plan,
      (p_sections IS NULL OR 'seq_dfip' = ANY(p_sections)) AS s_seq_d,
      (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr,
      (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs,
      (p_sections IS NULL OR 'allocation' = ANY(p_sections))      AS s_al,
      (p_sections IS NULL OR 'reserve' = ANY(p_sections))         AS s_rsv,
      (p_sections IS NULL OR 'collegial' = ANY(p_sections))       AS s_col,
      (p_sections IS NULL OR 'ckk' = ANY(p_sections))             AS s_ckk,
      (p_sections IS NULL OR 'contract' = ANY(p_sections)) AS s_cd,
      (p_sections IS NULL OR 'booking'  = ANY(p_sections)) AS s_book,
      (p_sections IS NULL OR 'q1'       = ANY(p_sections)) AS s_q1,
      (p_sections IS NULL OR 'q2'       = ANY(p_sections)) AS s_q2,
      (p_sections IS NULL OR 'q3'       = ANY(p_sections)) AS s_q3,
      (p_sections IS NULL OR 'q4'       = ANY(p_sections)) AS s_q4,
      (p_sections IS NULL OR 'totals'   = ANY(p_sections)) AS s_tot
  )
  SELECT
    s.row_type, s.depth, s.line_id,

    -- header (всегда). expense_item_id из _sort_path (см. v_form1_jsonb).
    jsonb_build_object(
      'section_code',    s.col_section_code,
      'item_id',         s.col_item_id,
      'num_group',       s.col_num_group_id,
      'name',            s.col_name,
      'justification',   s.col_justification,
      'internal_order',  s.col_internal_order,
      'vsp_id',          s.col_vsp_id,
      'vsp_address',     s.col_vsp_address,
      'expense_item_id', CASE WHEN s.row_type = 'INPUT'
                           THEN s._sort_path[array_upper(s._sort_path,1)-1]
                           ELSE s._sort_path[array_upper(s._sort_path,1)] END
    ) AS header,

    CASE WHEN f.s_plan THEN jsonb_build_object(
      'q1',      s.col_plan_q1, 'q2', s.col_plan_q2,
      'q3',      s.col_plan_q3, 'q4', s.col_plan_q4,
      'year',    s.col_plan_year, 'comment', s.col_plan_comment
    ) END AS plan_data,

    CASE WHEN f.s_seq_d THEN jsonb_build_object(
      'q1',     s.col_seq_dfip_q1, 'q2', s.col_seq_dfip_q2,
      'q3',     s.col_seq_dfip_q3, 'q4', s.col_seq_dfip_q4,
      'year',   s.col_seq_dfip_year,
      'justification', s.col_seq_dfip_just
    ) END AS seq_dfip_data,

    CASE WHEN f.s_appr THEN jsonb_build_object(
      'q1',   s.col_appr_q1, 'q2', s.col_appr_q2,
      'q3',   s.col_appr_q3, 'q4', s.col_appr_q4,
      'year', s.col_appr_year
    ) END AS approved_data,

    CASE WHEN f.s_cs THEN jsonb_build_object(
      'amount',           s.col_cs_amount,
      'reference',        s.col_cs_reference,
      'counterparty',     s.col_cs_counterparty,
      'deadline',         s.col_cs_deadline,
      'comment',          s.col_cs_comment,
      'future_payments_y1', s.col_cs_future_y1,
      'future_payments_y2', s.col_cs_future_y2,
      'other_ssp_amount', s.col_cs_other_ssp,
      'centralized_flag', s.col_cs_centralized_flag
    ) END AS contract_summary_data,

    CASE WHEN f.s_al THEN jsonb_build_object(
      'contract_ref',       s.col_al_contract_ref,
      'allocation_purpose', s.col_al_purpose
    ) END AS allocation_data,

    CASE WHEN f.s_rsv THEN jsonb_build_object(
      'q1',   s.col_rsv_q1, 'q2', s.col_rsv_q2,
      'q3',   s.col_rsv_q3, 'q4', s.col_rsv_q4,
      'year', s.col_rsv_year,
      'justification', s.col_rsv_just
    ) END AS reserve_data,

    CASE WHEN f.s_col THEN jsonb_build_object(
      'amount',             s.col_col_amount,
      'protocol_reference', s.col_col_protocol,
      'note',               s.col_col_note
    ) END AS collegial_data,

    CASE WHEN f.s_ckk THEN jsonb_build_object(
      'ceiling',            s.col_ckk_ceiling,
      'expenses_q1',        s.col_ckk_q1,
      'expenses_q2',        s.col_ckk_q2,
      'expenses_q3',        s.col_ckk_q3,
      'expenses_q4',        s.col_ckk_q4,
      'rf_schedule',        s.col_ckk_rf_schedule,
      'delivery_deadline',  s.col_ckk_delivery_deadline,
      'procurement_plan',   s.col_ckk_procurement_plan,
      'procurement_method', s.col_ckk_procurement_method,
      'comment',            s.col_ckk_comment
    ) END AS ckk_data,

    CASE WHEN f.s_cd THEN jsonb_build_object(
      'counterparty',     s.col_cd_counterparty,
      'reference',        s.col_cd_reference,
      'addenda',          s.col_cd_addenda,
      'date',             s.col_cd_date,
      'subject',          s.col_cd_subject,
      'currency',         s.col_cd_currency,
      'ceiling',          s.col_cd_ceiling,
      'expenses_q1',      s.col_cd_q1,
      'expenses_q2',      s.col_cd_q2,
      'expenses_q3',      s.col_cd_q3,
      'expenses_q4',      s.col_cd_q4,
      'rf_schedule',      s.col_cd_rf_schedule,
      'vat_rate',         s.col_cd_vat_rate,
      'exchange_rate',    s.col_cd_exchange_rate,
      'amount_foreign',   s.col_cd_amount_foreign,
      'deadline',         s.col_cd_deadline,
      'scheme',           s.col_cd_scheme,
      'act',              s.col_cd_act,
      'comment',          s.col_cd_comment
    ) END AS contract_data,

    CASE WHEN f.s_book THEN jsonb_build_object(
      'y2026', jsonb_build_object('q1', s.col_book_q1, 'q2', s.col_book_q2, 'q3', s.col_book_q3, 'q4', s.col_book_q4),
      'y2027', jsonb_build_object('q1', s.col_book_next_q1, 'q2', s.col_book_next_q2, 'q3', s.col_book_next_q3, 'q4', s.col_book_next_q4)
    ) END AS booking_data,

    CASE WHEN f.s_q1 THEN jsonb_build_object(
      'adj_current',    s.col_q1_adj_current,
      'adj_ssp',        s.col_q1_adj_ssp,
      'adj_rf',         s.col_q1_adj_rf,
      'adj_reserve',    s.col_q1_adj_reserve,
      'adj_comment',    s.col_q1_adj_comment,
      'corrected_plan', s.col_q1_corrected_plan,
      'pay_date',       s.col_q1_pay_date,
      'pay_amount',     s.col_q1_pay_amount,
      'pay_ho',         s.col_q1_pay_ho,
      'pay_rf',         s.col_q1_pay_rf,
      'pay_comment',    s.col_q1_pay_comment,
      'pay_act',        s.col_q1_pay_act,
      'booking',        s.col_q1_booking,
      'actual_m1',      s.col_q1_actual_m1,
      'actual_m2',      s.col_q1_actual_m2,
      'actual_m3',      s.col_q1_actual_m3,
      'actual_quarter', s.col_q1_actual_quarter,
      'residual_after_booking', s.col_q1_residual_after_booking,
      'residual_after_actual',  s.col_q1_residual_after_actual,
      'transfer_q2',    s.col_q1_transfer_q2,
      'transfer_q3',    s.col_q1_transfer_q3,
      'transfer_q4',    s.col_q1_transfer_q4,
      'transfer_econ',  s.col_q1_transfer_econ,
      'total',          s.col_q1_total
    ) END AS q1_data,

    CASE WHEN f.s_q2 THEN jsonb_build_object(
      'target_change',           s.col_q2_target_change,
      'base_correction',         s.col_q2_base_correction,
      'base_correction_comment', s.col_q2_base_correction_comment,
      'revision_inc',            s.col_q2_revision_inc,
      'revision_seq',            s.col_q2_revision_seq,
      'revision_comment',        s.col_q2_revision_comment,
      'new_plan',                s.col_q2_new_plan,
      'adj_current',             s.col_q2_adj_current,
      'adj_ssp',                 s.col_q2_adj_ssp,
      'adj_rf',                  s.col_q2_adj_rf,
      'adj_reserve',             s.col_q2_adj_reserve,
      'adj_comment',             s.col_q2_adj_comment,
      'corrected_plan',          s.col_q2_corrected_plan,
      'pay_date',                s.col_q2_pay_date,
      'pay_amount',              s.col_q2_pay_amount,
      'pay_ho',                  s.col_q2_pay_ho,
      'pay_rf',                  s.col_q2_pay_rf,
      'pay_comment',             s.col_q2_pay_comment,
      'pay_act',                 s.col_q2_pay_act,
      'booking',                 s.col_q2_booking,
      'actual_m1',               s.col_q2_actual_m1,
      'actual_m2',               s.col_q2_actual_m2,
      'actual_m3',               s.col_q2_actual_m3,
      'actual_quarter',          s.col_q2_actual_quarter,
      'residual_after_booking',  s.col_q2_residual_after_booking,
      'residual_after_actual',   s.col_q2_residual_after_actual,
      'transfer_q3',             s.col_q2_transfer_q3,
      'transfer_q4',             s.col_q2_transfer_q4,
      'transfer_econ',           s.col_q2_transfer_econ,
      'total',                   s.col_q2_total
    ) END AS q2_data,

    CASE WHEN f.s_q3 THEN jsonb_build_object(
      'target_change',           s.col_q3_target_change,
      'base_correction',         s.col_q3_base_correction,
      'base_correction_comment', s.col_q3_base_correction_comment,
      'revision_inc',            s.col_q3_revision_inc,
      'revision_seq',            s.col_q3_revision_seq,
      'revision_comment',        s.col_q3_revision_comment,
      'new_plan',                s.col_q3_new_plan,
      'adj_current',             s.col_q3_adj_current,
      'adj_ssp',                 s.col_q3_adj_ssp,
      'adj_rf',                  s.col_q3_adj_rf,
      'adj_reserve',             s.col_q3_adj_reserve,
      'adj_comment',             s.col_q3_adj_comment,
      'corrected_plan',          s.col_q3_corrected_plan,
      'pay_date',                s.col_q3_pay_date,
      'pay_amount',              s.col_q3_pay_amount,
      'pay_ho',                  s.col_q3_pay_ho,
      'pay_rf',                  s.col_q3_pay_rf,
      'pay_comment',             s.col_q3_pay_comment,
      'pay_act',                 s.col_q3_pay_act,
      'booking',                 s.col_q3_booking,
      'actual_m1',               s.col_q3_actual_m1,
      'actual_m2',               s.col_q3_actual_m2,
      'actual_m3',               s.col_q3_actual_m3,
      'actual_quarter',          s.col_q3_actual_quarter,
      'residual_after_booking',  s.col_q3_residual_after_booking,
      'residual_after_actual',   s.col_q3_residual_after_actual,
      'transfer_q4',             s.col_q3_transfer_q4,
      'transfer_econ',           s.col_q3_transfer_econ,
      'total',                   s.col_q3_total
    ) END AS q3_data,

    CASE WHEN f.s_q4 THEN jsonb_build_object(
      'target_change',           s.col_q4_target_change,
      'base_correction',         s.col_q4_base_correction,
      'base_correction_comment', s.col_q4_base_correction_comment,
      'revision_inc',            s.col_q4_revision_inc,
      'revision_seq',            s.col_q4_revision_seq,
      'revision_comment',        s.col_q4_revision_comment,
      'new_plan',                s.col_q4_new_plan,
      'adj_current',             s.col_q4_adj_current,
      'adj_ssp',                 s.col_q4_adj_ssp,
      'adj_rf',                  s.col_q4_adj_rf,
      'adj_reserve',             s.col_q4_adj_reserve,
      'adj_comment',             s.col_q4_adj_comment,
      'corrected_plan',          s.col_q4_corrected_plan,
      'pay_date',                s.col_q4_pay_date,
      'pay_amount',              s.col_q4_pay_amount,
      'pay_ho',                  s.col_q4_pay_ho,
      'pay_rf',                  s.col_q4_pay_rf,
      'pay_comment',             s.col_q4_pay_comment,
      'pay_act',                 s.col_q4_pay_act,
      'booking',                 s.col_q4_booking,
      'actual_m1',               s.col_q4_actual_m1,
      'actual_m2',               s.col_q4_actual_m2,
      'actual_m3',               s.col_q4_actual_m3,
      'actual_spod',             s.col_q4_actual_spod,
      'actual_quarter',          s.col_q4_actual_quarter,
      'residual_after_booking',  s.col_q4_residual_after_booking,
      'residual_after_actual',   s.col_q4_residual_after_actual,
      'transfer_econ',           s.col_q4_transfer_econ,
      'total',                   s.col_q4_total
    ) END AS q4_data,

    CASE WHEN f.s_tot THEN jsonb_build_object(
      'fact_year',    s.col_fact_year
    ) END AS totals_data,

    s._sort_path
  FROM v3.v_form4_sheet_sections(p_form_id, p_sheet, p_sections) s
  CROSS JOIN flags f
  ORDER BY s._sort_path;
$function$
;

-- DROP FUNCTION v3.v_form4_sheet_jsonb(int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE sql
 STABLE
AS $function$
  -- 1. Базовые INPUT-чанки + project / section расширение
  WITH inp_raw AS (
    SELECT j.*
    FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j
    WHERE j.row_type = 'INPUT'
  ),
  inp AS (
    SELECT
      i.*,
      bl.project_id                                AS line_project_id,
      pp.id   AS prj_id,   pp.name AS prj_name,
      pg.id   AS prog_id,  pg.name AS prog_name,
      ei.id   AS ei_id,    ei.section_code,
      -- ROOT: ищем depth=0 предка через recursive CTE inline
      sec.id  AS sec_id,   sec.section_code AS sec_code, sec.name AS sec_name
    FROM inp_raw i
    JOIN v3.budget_line bl ON bl.id = i.line_id
    JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN v3.project pp ON pp.id = bl.project_id AND pp.level = 'project'
    LEFT JOIN v3.project pg ON pg.id = pp.parent_id  AND pg.level = 'program'
    LEFT JOIN LATERAL (
      WITH RECURSIVE up AS (
        SELECT id, parent_id, name, section_code, depth FROM v3.expense_item WHERE id = ei.id
        UNION ALL
        SELECT e.id, e.parent_id, e.name, e.section_code, e.depth
        FROM v3.expense_item e JOIN up u ON u.parent_id = e.id
      )
      SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1
    ) sec ON TRUE
  ),
  -- 2. PROJECT агрегаты
  prj AS (
    SELECT
      sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
      prog_id, MAX(prog_name) AS prog_name,
      prj_id,  MAX(prj_name)  AS prj_name,
      v3.jsonb_sum(plan_data)              AS plan_data,
      v3.jsonb_sum(seq_dfip_data)          AS seq_dfip_data,
      v3.jsonb_sum(approved_data)          AS approved_data,
      v3.jsonb_sum(contract_summary_data)  AS contract_summary_data,
      v3.jsonb_sum(allocation_data)        AS allocation_data,
      v3.jsonb_sum(reserve_data)           AS reserve_data,
      v3.jsonb_sum(collegial_data)         AS collegial_data,
      v3.jsonb_sum(ckk_data)               AS ckk_data,
      v3.jsonb_sum(contract_data)          AS contract_data,
      v3.jsonb_sum(booking_data)           AS booking_data,
      v3.jsonb_sum(q1_data)       AS q1_data,
      v3.jsonb_sum(q2_data)       AS q2_data,
      v3.jsonb_sum(q3_data)       AS q3_data,
      v3.jsonb_sum(q4_data)       AS q4_data,
      v3.jsonb_sum(totals_data)   AS totals_data
    FROM inp WHERE prj_id IS NOT NULL
    GROUP BY sec_id, prog_id, prj_id
  ),
  -- 3. PROGRAM агрегаты
  prog AS (
    SELECT
      sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
      prog_id, MAX(prog_name) AS prog_name,
      v3.jsonb_sum(plan_data)              AS plan_data,
      v3.jsonb_sum(seq_dfip_data)          AS seq_dfip_data,
      v3.jsonb_sum(approved_data)          AS approved_data,
      v3.jsonb_sum(contract_summary_data)  AS contract_summary_data,
      v3.jsonb_sum(allocation_data)        AS allocation_data,
      v3.jsonb_sum(reserve_data)           AS reserve_data,
      v3.jsonb_sum(collegial_data)         AS collegial_data,
      v3.jsonb_sum(ckk_data)               AS ckk_data,
      v3.jsonb_sum(contract_data)          AS contract_data,
      v3.jsonb_sum(booking_data)           AS booking_data,
      v3.jsonb_sum(q1_data)       AS q1_data,
      v3.jsonb_sum(q2_data)       AS q2_data,
      v3.jsonb_sum(q3_data)       AS q3_data,
      v3.jsonb_sum(q4_data)       AS q4_data,
      v3.jsonb_sum(totals_data)   AS totals_data
    FROM inp WHERE prog_id IS NOT NULL
    GROUP BY sec_id, prog_id
  ),
  -- 4. ROOT (section) агрегаты по всем строкам секции (с проектом и без)
  root AS (
    SELECT
      sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
      v3.jsonb_sum(plan_data)              AS plan_data,
      v3.jsonb_sum(seq_dfip_data)          AS seq_dfip_data,
      v3.jsonb_sum(approved_data)          AS approved_data,
      v3.jsonb_sum(contract_summary_data)  AS contract_summary_data,
      v3.jsonb_sum(allocation_data)        AS allocation_data,
      v3.jsonb_sum(reserve_data)           AS reserve_data,
      v3.jsonb_sum(collegial_data)         AS collegial_data,
      v3.jsonb_sum(ckk_data)               AS ckk_data,
      v3.jsonb_sum(contract_data)          AS contract_data,
      v3.jsonb_sum(booking_data)           AS booking_data,
      v3.jsonb_sum(q1_data)       AS q1_data,
      v3.jsonb_sum(q2_data)       AS q2_data,
      v3.jsonb_sum(q3_data)       AS q3_data,
      v3.jsonb_sum(q4_data)       AS q4_data,
      v3.jsonb_sum(totals_data)   AS totals_data
    FROM inp
    GROUP BY sec_id
  ),
  -- 5. Все 4 уровня в едином формате (sort_path для упорядочения)
  unioned AS (
    SELECT
      'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id,
      jsonb_build_object('section_code', sec_code, 'name', sec_name) AS header,
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path
    FROM root
    UNION ALL
    SELECT
      'GROUP', 1, NULL,
      jsonb_build_object('section_code', sec_code, 'name', prog_name,
                         'program_id',   prog_id),
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, prog_id, 0, 0]::INT[]
    FROM prog
    UNION ALL
    SELECT
      'ITEM', 2, NULL,
      jsonb_build_object('section_code', sec_code, 'name', prj_name,
                         'program_id',   prog_id, 'project_id', prj_id),
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
    FROM prj
    UNION ALL
    SELECT
      'INPUT', 3, line_id,
      header || jsonb_build_object(
        'project_id', prj_id, 'project_name', prj_name,
        'program_id', prog_id, 'program_name', prog_name
      ),
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(prj_id, 999999), line_id]::INT[]
    FROM inp
  )
  SELECT
    u.row_type, u.depth,
    ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order,
    jsonb_build_object(
      'line_id',          u.line_id,
      'header',           u.header,
      'plan',             u.plan_data,
      'seq_dfip',         u.seq_dfip_data,
      'approved',         u.approved_data,
      'contract_summary', u.contract_summary_data,
      'allocation',       u.allocation_data,
      'reserve',          u.reserve_data,
      'collegial',        u.collegial_data,
      'ckk',              u.ckk_data,
      'contract',         u.contract_data,
      'booking',          u.booking_data,
      'q1',               u.q1_data,
      'q2',               u.q2_data,
      'q3',               u.q3_data,
      'q4',               u.q4_data,
      'totals',           u.totals_data
    ) AS data
  FROM unioned u
  ORDER BY u.sort_path;
$function$
;

-- DROP FUNCTION v3.v_form4_smeta(int4);

CREATE OR REPLACE FUNCTION v3.v_form4_smeta(p_form_id integer)
 RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, plan_supp_q1 numeric, plan_supp_q2 numeric, plan_supp_q3 numeric, plan_supp_q4 numeric, plan_supp_year numeric, plan_dev_q1 numeric, plan_dev_q2 numeric, plan_dev_q3 numeric, plan_dev_q4 numeric, plan_dev_year numeric, appr_supp_q1 numeric, appr_supp_q2 numeric, appr_supp_q3 numeric, appr_supp_q4 numeric, appr_supp_year numeric, appr_dev_q1 numeric, appr_dev_q2 numeric, appr_dev_q3 numeric, appr_dev_q4 numeric, appr_dev_year numeric, fact_supp_q1 numeric, fact_supp_q2 numeric, fact_supp_q3 numeric, fact_supp_q4 numeric, fact_supp_year numeric, fact_dev_q1 numeric, fact_dev_q2 numeric, fact_dev_q3 numeric, fact_dev_q4 numeric, fact_dev_year numeric, corr_supp_q1 numeric, corr_supp_q2 numeric, corr_supp_q3 numeric, corr_supp_q4 numeric, corr_supp_year numeric, corr_dev_q1 numeric, corr_dev_q2 numeric, corr_dev_q3 numeric, corr_dev_q4 numeric, corr_dev_year numeric)
 LANGUAGE plpgsql
 STABLE
AS $function$
#variable_conflict use_column
BEGIN
  RETURN QUERY
  WITH
    -- Per-line per-direction агрегаты квартальных данных
    q_per_line AS (
      SELECT line_id,
        SUM(CASE WHEN quarter=1 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq1,
        SUM(CASE WHEN quarter=2 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq2,
        SUM(CASE WHEN quarter=3 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0) END) AS fq3,
        SUM(CASE WHEN quarter=4 THEN COALESCE(actual_m1,0)+COALESCE(actual_m2,0)+COALESCE(actual_m3,0)+COALESCE(actual_spod,0) END) AS fq4,
        SUM(CASE WHEN quarter=1 THEN transfer_to_q2 END) AS t1q2,
        SUM(CASE WHEN quarter=1 THEN transfer_to_q3 END) AS t1q3,
        SUM(CASE WHEN quarter=1 THEN transfer_to_q4 END) AS t1q4,
        SUM(CASE WHEN quarter=2 THEN transfer_to_q3 END) AS t2q3,
        SUM(CASE WHEN quarter=2 THEN transfer_to_q4 END) AS t2q4,
        SUM(CASE WHEN quarter=3 THEN transfer_to_q4 END) AS t3q4,
        SUM(CASE WHEN quarter=1 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj1,
        SUM(CASE WHEN quarter=2 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj2,
        SUM(CASE WHEN quarter=3 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj3,
        SUM(CASE WHEN quarter=4 THEN COALESCE(adj_current,0)+COALESCE(adj_ssp,0)+COALESCE(adj_reserve,0) END) AS adj4,
        -- Δ approved → new_plan: revision_inc/seq + target_change + base_correction
        -- (согласовано с v_form4_sheet_sections / Excel DJ10 = SUM(AN, CY, DH, CF, DE:DG))
        SUM(CASE WHEN quarter=2 THEN COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0)+COALESCE(target_change,0)+COALESCE(base_plan_correction,0) END) AS rev2,
        SUM(CASE WHEN quarter=3 THEN COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0)+COALESCE(target_change,0)+COALESCE(base_plan_correction,0) END) AS rev3,
        SUM(CASE WHEN quarter=4 THEN COALESCE(plan_revision_increase,0)+COALESCE(plan_revision_sequester,0)+COALESCE(target_change,0)+COALESCE(base_plan_correction,0) END) AS rev4
      FROM v3.budget_line_quarter GROUP BY line_id
    ),
    seq_dfip AS (
      SELECT line_id, SUM(adj_q1) AS s1, SUM(adj_q2) AS s2, SUM(adj_q3) AS s3, SUM(adj_q4) AS s4
      FROM v3.sequestration WHERE actor='DFIP' GROUP BY line_id
    ),
    seq_ssp AS (
      SELECT line_id, SUM(adj_q1) AS g1, SUM(adj_q2) AS g2, SUM(adj_q3) AS g3, SUM(adj_q4) AS g4
      FROM v3.sequestration WHERE actor='SSP_GO' GROUP BY line_id
    ),
    -- Per-line aggregate.
    -- FORM_4 = "Расходы на развитие" (АХР_Р!F5='ПРОЕКТНАЯ ДЕЯТЕЛЬНОСТЬ' в Excel),
    -- поэтому все строки идут в колонку Development независимо от
    -- expense_item.direction (этот справочник общий с FORM_2 и для FORM_2
    -- проставлен 'Support'). Колонка Support по форме 4 всегда = 0.
    line_agg AS (
      SELECT
        bl.expense_item_id AS eid, 'Development'::VARCHAR AS dir,
        SUM(p.plan_q1) AS pq1, SUM(p.plan_q2) AS pq2, SUM(p.plan_q3) AS pq3, SUM(p.plan_q4) AS pq4,
        SUM(sd.s1) AS sd1, SUM(sd.s2) AS sd2, SUM(sd.s3) AS sd3, SUM(sd.s4) AS sd4,
        SUM(sg.g1) AS sg1, SUM(sg.g2) AS sg2, SUM(sg.g3) AS sg3, SUM(sg.g4) AS sg4,
        SUM(r.amount_q1) AS rq1, SUM(r.amount_q2) AS rq2, SUM(r.amount_q3) AS rq3, SUM(r.amount_q4) AS rq4,
        SUM(qp.fq1) AS fq1, SUM(qp.fq2) AS fq2, SUM(qp.fq3) AS fq3, SUM(qp.fq4) AS fq4,
        SUM(qp.t1q2) AS t1q2, SUM(qp.t1q3) AS t1q3, SUM(qp.t1q4) AS t1q4,
        SUM(qp.t2q3) AS t2q3, SUM(qp.t2q4) AS t2q4, SUM(qp.t3q4) AS t3q4,
        SUM(qp.adj1) AS adj1, SUM(qp.adj2) AS adj2, SUM(qp.adj3) AS adj3, SUM(qp.adj4) AS adj4,
        SUM(qp.rev2) AS rev2, SUM(qp.rev3) AS rev3, SUM(qp.rev4) AS rev4
      FROM v3.budget_line bl
      LEFT JOIN v3.plan p          ON p.line_id  = bl.id
      LEFT JOIN seq_dfip sd        ON sd.line_id = bl.id
      LEFT JOIN seq_ssp sg         ON sg.line_id = bl.id
      LEFT JOIN v3.reserve r       ON r.line_id  = bl.id
      LEFT JOIN q_per_line qp      ON qp.line_id = bl.id
      WHERE bl.budget_form_id = p_form_id
      GROUP BY bl.expense_item_id
    ),
    -- Tree rollup с расщеплением по direction. Используем FILTER (WHERE)
    tw_rollup AS (
      SELECT
        t.id, t.depth, t.item_id AS section_code, t.name,
        -- Поддержка
        SUM(a.pq1) FILTER (WHERE a.dir='Support') AS s_pq1,
        SUM(a.pq2) FILTER (WHERE a.dir='Support') AS s_pq2,
        SUM(a.pq3) FILTER (WHERE a.dir='Support') AS s_pq3,
        SUM(a.pq4) FILTER (WHERE a.dir='Support') AS s_pq4,
        SUM(a.sd1) FILTER (WHERE a.dir='Support') AS s_sd1,
        SUM(a.sd2) FILTER (WHERE a.dir='Support') AS s_sd2,
        SUM(a.sd3) FILTER (WHERE a.dir='Support') AS s_sd3,
        SUM(a.sd4) FILTER (WHERE a.dir='Support') AS s_sd4,
        SUM(a.sg1) FILTER (WHERE a.dir='Support') AS s_sg1,
        SUM(a.sg2) FILTER (WHERE a.dir='Support') AS s_sg2,
        SUM(a.sg3) FILTER (WHERE a.dir='Support') AS s_sg3,
        SUM(a.sg4) FILTER (WHERE a.dir='Support') AS s_sg4,
        SUM(a.rq1) FILTER (WHERE a.dir='Support') AS s_rq1,
        SUM(a.rq2) FILTER (WHERE a.dir='Support') AS s_rq2,
        SUM(a.rq3) FILTER (WHERE a.dir='Support') AS s_rq3,
        SUM(a.rq4) FILTER (WHERE a.dir='Support') AS s_rq4,
        SUM(a.fq1) FILTER (WHERE a.dir='Support') AS s_fq1,
        SUM(a.fq2) FILTER (WHERE a.dir='Support') AS s_fq2,
        SUM(a.fq3) FILTER (WHERE a.dir='Support') AS s_fq3,
        SUM(a.fq4) FILTER (WHERE a.dir='Support') AS s_fq4,
        SUM(a.t1q2) FILTER (WHERE a.dir='Support') AS s_t1q2,
        SUM(a.t1q3) FILTER (WHERE a.dir='Support') AS s_t1q3,
        SUM(a.t1q4) FILTER (WHERE a.dir='Support') AS s_t1q4,
        SUM(a.t2q3) FILTER (WHERE a.dir='Support') AS s_t2q3,
        SUM(a.t2q4) FILTER (WHERE a.dir='Support') AS s_t2q4,
        SUM(a.t3q4) FILTER (WHERE a.dir='Support') AS s_t3q4,
        SUM(a.adj1) FILTER (WHERE a.dir='Support') AS s_adj1,
        SUM(a.adj2) FILTER (WHERE a.dir='Support') AS s_adj2,
        SUM(a.adj3) FILTER (WHERE a.dir='Support') AS s_adj3,
        SUM(a.adj4) FILTER (WHERE a.dir='Support') AS s_adj4,
        SUM(a.rev2) FILTER (WHERE a.dir='Support') AS s_rev2,
        SUM(a.rev3) FILTER (WHERE a.dir='Support') AS s_rev3,
        SUM(a.rev4) FILTER (WHERE a.dir='Support') AS s_rev4,
        -- Развитие
        SUM(a.pq1) FILTER (WHERE a.dir='Development') AS d_pq1,
        SUM(a.pq2) FILTER (WHERE a.dir='Development') AS d_pq2,
        SUM(a.pq3) FILTER (WHERE a.dir='Development') AS d_pq3,
        SUM(a.pq4) FILTER (WHERE a.dir='Development') AS d_pq4,
        SUM(a.sd1) FILTER (WHERE a.dir='Development') AS d_sd1,
        SUM(a.sd2) FILTER (WHERE a.dir='Development') AS d_sd2,
        SUM(a.sd3) FILTER (WHERE a.dir='Development') AS d_sd3,
        SUM(a.sd4) FILTER (WHERE a.dir='Development') AS d_sd4,
        SUM(a.sg1) FILTER (WHERE a.dir='Development') AS d_sg1,
        SUM(a.sg2) FILTER (WHERE a.dir='Development') AS d_sg2,
        SUM(a.sg3) FILTER (WHERE a.dir='Development') AS d_sg3,
        SUM(a.sg4) FILTER (WHERE a.dir='Development') AS d_sg4,
        SUM(a.rq1) FILTER (WHERE a.dir='Development') AS d_rq1,
        SUM(a.rq2) FILTER (WHERE a.dir='Development') AS d_rq2,
        SUM(a.rq3) FILTER (WHERE a.dir='Development') AS d_rq3,
        SUM(a.rq4) FILTER (WHERE a.dir='Development') AS d_rq4,
        SUM(a.fq1) FILTER (WHERE a.dir='Development') AS d_fq1,
        SUM(a.fq2) FILTER (WHERE a.dir='Development') AS d_fq2,
        SUM(a.fq3) FILTER (WHERE a.dir='Development') AS d_fq3,
        SUM(a.fq4) FILTER (WHERE a.dir='Development') AS d_fq4,
        SUM(a.t1q2) FILTER (WHERE a.dir='Development') AS d_t1q2,
        SUM(a.t1q3) FILTER (WHERE a.dir='Development') AS d_t1q3,
        SUM(a.t1q4) FILTER (WHERE a.dir='Development') AS d_t1q4,
        SUM(a.t2q3) FILTER (WHERE a.dir='Development') AS d_t2q3,
        SUM(a.t2q4) FILTER (WHERE a.dir='Development') AS d_t2q4,
        SUM(a.t3q4) FILTER (WHERE a.dir='Development') AS d_t3q4,
        SUM(a.adj1) FILTER (WHERE a.dir='Development') AS d_adj1,
        SUM(a.adj2) FILTER (WHERE a.dir='Development') AS d_adj2,
        SUM(a.adj3) FILTER (WHERE a.dir='Development') AS d_adj3,
        SUM(a.adj4) FILTER (WHERE a.dir='Development') AS d_adj4,
        SUM(a.rev2) FILTER (WHERE a.dir='Development') AS d_rev2,
        SUM(a.rev3) FILTER (WHERE a.dir='Development') AS d_rev3,
        SUM(a.rev4) FILTER (WHERE a.dir='Development') AS d_rev4
      FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id
      JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code
      LEFT JOIN line_agg a ON a.eid = ANY(t.desc_ids)
      WHERE t.depth <= 2
      GROUP BY t.id, t.depth, t.item_id, t.name
    )
  SELECT
    CASE r.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' END::VARCHAR,
    r.depth,
    r.section_code::VARCHAR,
    r.name::VARCHAR,
    -- Plan supp / dev
    r.s_pq1, r.s_pq2, r.s_pq3, r.s_pq4,
    COALESCE(r.s_pq1,0)+COALESCE(r.s_pq2,0)+COALESCE(r.s_pq3,0)+COALESCE(r.s_pq4,0),
    r.d_pq1, r.d_pq2, r.d_pq3, r.d_pq4,
    COALESCE(r.d_pq1,0)+COALESCE(r.d_pq2,0)+COALESCE(r.d_pq3,0)+COALESCE(r.d_pq4,0),
    -- Approved supp = plan + seq_dfip + seq_ssp + reserve
    COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0),
    COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0),
    COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0),
    COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0),
    COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0)
      +COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0)
      +COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0)
      +COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0),
    -- Approved dev
    COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0),
    COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0),
    COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0),
    COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0),
    COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0)
      +COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0)
      +COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0)
      +COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0),
    -- Fact supp / dev
    r.s_fq1, r.s_fq2, r.s_fq3, r.s_fq4,
    COALESCE(r.s_fq1,0)+COALESCE(r.s_fq2,0)+COALESCE(r.s_fq3,0)+COALESCE(r.s_fq4,0),
    r.d_fq1, r.d_fq2, r.d_fq3, r.d_fq4,
    COALESCE(r.d_fq1,0)+COALESCE(r.d_fq2,0)+COALESCE(r.d_fq3,0)+COALESCE(r.d_fq4,0),
    -- Corrected supp = approved + transfers + revisions + adj
    COALESCE(r.s_pq1,0)+COALESCE(r.s_sd1,0)+COALESCE(r.s_sg1,0)+COALESCE(r.s_rq1,0)
      +COALESCE(r.s_adj1,0),
    COALESCE(r.s_pq2,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_rq2,0)
      +COALESCE(r.s_t1q2,0)+COALESCE(r.s_rev2,0)+COALESCE(r.s_adj2,0),
    COALESCE(r.s_pq3,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_rq3,0)
      +COALESCE(r.s_t1q3,0)+COALESCE(r.s_t2q3,0)+COALESCE(r.s_rev3,0)+COALESCE(r.s_adj3,0),
    COALESCE(r.s_pq4,0)+COALESCE(r.s_sd4,0)+COALESCE(r.s_sg4,0)+COALESCE(r.s_rq4,0)
      +COALESCE(r.s_t1q4,0)+COALESCE(r.s_t2q4,0)+COALESCE(r.s_t3q4,0)+COALESCE(r.s_rev4,0)+COALESCE(r.s_adj4,0),
    COALESCE(r.s_pq1,0)+COALESCE(r.s_pq2,0)+COALESCE(r.s_pq3,0)+COALESCE(r.s_pq4,0)
      +COALESCE(r.s_sd1,0)+COALESCE(r.s_sd2,0)+COALESCE(r.s_sd3,0)+COALESCE(r.s_sd4,0)
      +COALESCE(r.s_sg1,0)+COALESCE(r.s_sg2,0)+COALESCE(r.s_sg3,0)+COALESCE(r.s_sg4,0)
      +COALESCE(r.s_rq1,0)+COALESCE(r.s_rq2,0)+COALESCE(r.s_rq3,0)+COALESCE(r.s_rq4,0)
      +COALESCE(r.s_adj1,0)+COALESCE(r.s_adj2,0)+COALESCE(r.s_adj3,0)+COALESCE(r.s_adj4,0)
      +COALESCE(r.s_rev2,0)+COALESCE(r.s_rev3,0)+COALESCE(r.s_rev4,0),
    -- Corrected dev
    COALESCE(r.d_pq1,0)+COALESCE(r.d_sd1,0)+COALESCE(r.d_sg1,0)+COALESCE(r.d_rq1,0)
      +COALESCE(r.d_adj1,0),
    COALESCE(r.d_pq2,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_rq2,0)
      +COALESCE(r.d_t1q2,0)+COALESCE(r.d_rev2,0)+COALESCE(r.d_adj2,0),
    COALESCE(r.d_pq3,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_rq3,0)
      +COALESCE(r.d_t1q3,0)+COALESCE(r.d_t2q3,0)+COALESCE(r.d_rev3,0)+COALESCE(r.d_adj3,0),
    COALESCE(r.d_pq4,0)+COALESCE(r.d_sd4,0)+COALESCE(r.d_sg4,0)+COALESCE(r.d_rq4,0)
      +COALESCE(r.d_t1q4,0)+COALESCE(r.d_t2q4,0)+COALESCE(r.d_t3q4,0)+COALESCE(r.d_rev4,0)+COALESCE(r.d_adj4,0),
    COALESCE(r.d_pq1,0)+COALESCE(r.d_pq2,0)+COALESCE(r.d_pq3,0)+COALESCE(r.d_pq4,0)
      +COALESCE(r.d_sd1,0)+COALESCE(r.d_sd2,0)+COALESCE(r.d_sd3,0)+COALESCE(r.d_sd4,0)
      +COALESCE(r.d_sg1,0)+COALESCE(r.d_sg2,0)+COALESCE(r.d_sg3,0)+COALESCE(r.d_sg4,0)
      +COALESCE(r.d_rq1,0)+COALESCE(r.d_rq2,0)+COALESCE(r.d_rq3,0)+COALESCE(r.d_rq4,0)
      +COALESCE(r.d_adj1,0)+COALESCE(r.d_adj2,0)+COALESCE(r.d_adj3,0)+COALESCE(r.d_adj4,0)
      +COALESCE(r.d_rev2,0)+COALESCE(r.d_rev3,0)+COALESCE(r.d_rev4,0)
  FROM tw_rollup r
  ORDER BY r.section_code;
END;
$function$
;

-- DROP FUNCTION v3.v_form4_view(int4, varchar, _text);

CREATE OR REPLACE FUNCTION v3.v_form4_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
 STABLE
AS $function$
BEGIN

  -- ═══ AHR / CAP / OPER — с проектной иерархией ════════════════════════════
  IF p_sheet IN ('AHR','CAP','OPER') THEN
    RETURN QUERY SELECT * FROM v3.v_form4_sheet_jsonb(p_form_id, p_sheet, p_sections);
    RETURN;
  END IF;

  -- ═══ Структура_Р ═══════════════════════════════════════════════════════
  -- AHR + CAP + OPER в одной выдаче, отсортировано по (section_code,
  -- num_group, _sort_path). БЕЗ проектной иерархии.
  IF p_sheet = 'STRUCTURE' THEN
    RETURN QUERY
    WITH all_sheets AS (
      SELECT * FROM v3.v_form4_sheet_lines_jsonb(p_form_id, 'AHR',  p_sections)
      UNION ALL
      SELECT * FROM v3.v_form4_sheet_lines_jsonb(p_form_id, 'CAP',  p_sections)
      UNION ALL
      SELECT * FROM v3.v_form4_sheet_lines_jsonb(p_form_id, 'OPER', p_sections)
    )
    SELECT
      j.row_type,
      j.depth,
      ROW_NUMBER() OVER (
        ORDER BY (j.header->>'section_code'),
                 (j.header->>'num_group') NULLS FIRST,
                 j._sort_path
      ) AS sort_order,
      jsonb_build_object(
        'line_id',          j.line_id,
        'header',           j.header,
        'plan',             j.plan_data,
        'seq_dfip',         j.seq_dfip_data,
        'approved',         j.approved_data,
        'contract_summary', j.contract_summary_data,
        'allocation',       j.allocation_data,
        'reserve',          j.reserve_data,
        'collegial',        j.collegial_data,
        'ckk',              j.ckk_data,
        'contract',         j.contract_data,
        'booking',          j.booking_data,
        'q1',               j.q1_data,
        'q2',               j.q2_data,
        'q3',               j.q3_data,
        'q4',               j.q4_data,
        'totals',           j.totals_data
      )
    FROM all_sheets j
    ORDER BY (j.header->>'section_code'),
             (j.header->>'num_group') NULLS FIRST,
             j._sort_path;
    RETURN;
  END IF;

  -- ═══ Смета_2026 (с разделением Поддержка/Развитие) ═════════════════════
  IF p_sheet = 'SMETA' THEN
    RETURN QUERY
    SELECT
      sm.row_type,
      sm.depth,
      ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order,
      jsonb_build_object(
        'section_code', sm.section_code,
        'name',         sm.name,
        'plan', jsonb_build_object(
          'support',     jsonb_build_object('q1', sm.plan_supp_q1, 'q2', sm.plan_supp_q2, 'q3', sm.plan_supp_q3, 'q4', sm.plan_supp_q4, 'year', sm.plan_supp_year),
          'development', jsonb_build_object('q1', sm.plan_dev_q1,  'q2', sm.plan_dev_q2,  'q3', sm.plan_dev_q3,  'q4', sm.plan_dev_q4,  'year', sm.plan_dev_year)
        ),
        'approved', jsonb_build_object(
          'support',     jsonb_build_object('q1', sm.appr_supp_q1, 'q2', sm.appr_supp_q2, 'q3', sm.appr_supp_q3, 'q4', sm.appr_supp_q4, 'year', sm.appr_supp_year),
          'development', jsonb_build_object('q1', sm.appr_dev_q1,  'q2', sm.appr_dev_q2,  'q3', sm.appr_dev_q3,  'q4', sm.appr_dev_q4,  'year', sm.appr_dev_year)
        ),
        'fact', jsonb_build_object(
          'support',     jsonb_build_object('q1', sm.fact_supp_q1, 'q2', sm.fact_supp_q2, 'q3', sm.fact_supp_q3, 'q4', sm.fact_supp_q4, 'year', sm.fact_supp_year),
          'development', jsonb_build_object('q1', sm.fact_dev_q1,  'q2', sm.fact_dev_q2,  'q3', sm.fact_dev_q3,  'q4', sm.fact_dev_q4,  'year', sm.fact_dev_year)
        ),
        'corrected', jsonb_build_object(
          'support',     jsonb_build_object('q1', sm.corr_supp_q1, 'q2', sm.corr_supp_q2, 'q3', sm.corr_supp_q3, 'q4', sm.corr_supp_q4, 'year', sm.corr_supp_year),
          'development', jsonb_build_object('q1', sm.corr_dev_q1,  'q2', sm.corr_dev_q2,  'q3', sm.corr_dev_q3,  'q4', sm.corr_dev_q4,  'year', sm.corr_dev_year)
        )
      )
    FROM v3.v_form4_smeta(p_form_id) sm
    ORDER BY sm.section_code;
    RETURN;
  END IF;

  RAISE EXCEPTION 'Unknown sheet for FORM_4: %', p_sheet
    USING HINT = 'Use AHR / CAP / OPER / SMETA / STRUCTURE';

END;
$function$
;

-- DROP FUNCTION v3.v_form_view(int4, varchar, _text, varchar);

CREATE OR REPLACE FUNCTION v3.v_form_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_direction character varying DEFAULT NULL::character varying)
 RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
 LANGUAGE plpgsql
 STABLE
AS $function$
DECLARE
    v_form_type   VARCHAR;
    v_year        INT;
    v_org_unit_id INT;
BEGIN
  -- Определяем form_type автоматически
  SELECT form_type_code, year, org_unit_id
    INTO v_form_type, v_year, v_org_unit_id
  FROM v3.budget_form
  WHERE id = p_form_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'budget_form id=% не существует в v3.budget_form', p_form_id;
  END IF;

  -- ═══ FORM_1 ═══════════════════════════════════════════════════════════════
  IF v_form_type = 'FORM_1' THEN

    IF p_sheet = 'SMETA' THEN
      -- v3.v_form1_smeta параметризуется year+org_unit_id
      RETURN QUERY
      SELECT
        sm.row_type,
        sm.depth,
        ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order,
        jsonb_build_object(
          'section_code', sm.section_code,
          'name',         sm.name,
          'plan', jsonb_build_object(
            'support',     jsonb_build_object('q1', sm.supp_plan_q1, 'q2', sm.supp_plan_q2,
                                              'q3', sm.supp_plan_q3, 'q4', sm.supp_plan_q4,
                                              'year', sm.supp_plan_year),
            'development', jsonb_build_object('q1', sm.dev_plan_q1, 'q2', sm.dev_plan_q2,
                                              'q3', sm.dev_plan_q3, 'q4', sm.dev_plan_q4,
                                              'year', sm.dev_plan_year),
            'total_year',  sm.total_plan_year),
          'approved', jsonb_build_object(
            'support',     jsonb_build_object('q1', sm.supp_appr_q1, 'q2', sm.supp_appr_q2,
                                              'q3', sm.supp_appr_q3, 'q4', sm.supp_appr_q4,
                                              'year', sm.supp_appr_year),
            'development', jsonb_build_object('q1', sm.dev_appr_q1, 'q2', sm.dev_appr_q2,
                                              'q3', sm.dev_appr_q3, 'q4', sm.dev_appr_q4,
                                              'year', sm.dev_appr_year),
            'total_year',  sm.total_appr_year),
          'fact', jsonb_build_object(
            'support',     jsonb_build_object('q1', sm.supp_act_q1, 'q2', sm.supp_act_q2,
                                              'q3', sm.supp_act_q3, 'q4', sm.supp_act_q4,
                                              'year', sm.supp_act_year),
            'development', jsonb_build_object('q1', sm.dev_act_q1, 'q2', sm.dev_act_q2,
                                              'q3', sm.dev_act_q3, 'q4', sm.dev_act_q4,
                                              'year', sm.dev_act_year),
            'total_year',  sm.total_act_year),
          'corrected', jsonb_build_object(
            'support',     jsonb_build_object('q2', sm.supp_corr_q2, 'q3', sm.supp_corr_q3, 'q4', sm.supp_corr_q4),
            'development', jsonb_build_object('q2', sm.dev_corr_q2,  'q3', sm.dev_corr_q3,  'q4', sm.dev_corr_q4))
        )
      FROM v3.v_form1_smeta(v_year, v_org_unit_id) sm
      ORDER BY sm.section_code;
      RETURN;
    END IF;

    IF p_sheet IN ('AHR','CAP','OPER') THEN
      RETURN QUERY
      SELECT
        j.row_type,
        j.depth,
        ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order,
        jsonb_build_object(
          'line_id',          j.line_id,
          'header',           j.header,
          'plan',             j.plan_data,
          'contract_summary', j.contract_summary,
          'allocation',       j.allocation_data,
          'sequestration',    j.sequestration_data,
          'reserve',          j.reserve_data,
          'approved',         j.approved_data,
          'collegial',        j.collegial_data,
          'ckk',              j.ckk_data,
          'contract_detail',  j.contract_detail,
          'q1',               j.q1_data,
          'q2',               j.q2_data,
          'q3',               j.q3_data,
          'q4',               j.q4_data,
          'totals',           j.totals_data
        )
      FROM v3.v_form1_sheet_jsonb(p_form_id, p_sheet, p_direction, p_sections) j
      ORDER BY j._sort_path;
      RETURN;
    END IF;

    RAISE EXCEPTION 'Unknown FORM_1 sheet: %', p_sheet
      USING HINT = 'Use AHR / CAP / OPER / SMETA';
  END IF;

  -- ═══ FORM_2 ═══════════════════════════════════════════════════════════════
  IF v_form_type = 'FORM_2' THEN
    RETURN QUERY SELECT * FROM v3.v_form2_view(p_form_id, p_sheet, p_sections);
    RETURN;
  END IF;

  -- ═══ FORM_4 ═══════════════════════════════════════════════════════════════
  IF v_form_type = 'FORM_4' THEN
    RETURN QUERY SELECT * FROM v3.v_form4_view(p_form_id, p_sheet, p_sections);
    RETURN;
  END IF;

  RAISE EXCEPTION 'Unsupported form_type=% for form_id=%', v_form_type, p_form_id;

END;
$function$
;



CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id INT)
RETURNS INTEGER
AS $function$
DECLARE
    v_report_type VARCHAR;
    v_inserted    INTEGER;
BEGIN
    SELECT report_type INTO v_report_type
      FROM v3.rf_project_report
     WHERE id = p_report_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
    END IF;

    INSERT INTO v3.form3_phase
        (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
    SELECT p_report_id, pt.phase_code, pt.role,
           pt.column_keys, pt.opens_at, pt.closes_at
      FROM v3.phase_template pt
     WHERE pt.form_type = 'FORM_3'
       AND pt.sheet     = v_report_type
    ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING;

    GET DIAGNOSTICS v_inserted = ROW_COUNT;
    RETURN v_inserted;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

CREATE OR REPLACE FUNCTION v3.user_in_report_org(p_user_id INT, p_report_id INT)
RETURNS BOOLEAN
AS $function$
DECLARE
    v_org INT;
BEGIN
    SELECT p.org_unit_id
      INTO v_org
      FROM v3.rf_project_report r
      JOIN v3.project           p ON p.id = r.project_id
     WHERE r.id = p_report_id;

    IF v_org IS NULL THEN
        RETURN TRUE;     -- проект без org_unit — пропускаем фильтр
    END IF;

    RETURN EXISTS (
        SELECT 1
          FROM v3.user_org
         WHERE user_id = p_user_id
           AND org_unit_id = v_org
    );
END;
$function$ 
LANGUAGE plpgsql STABLE;

CREATE OR REPLACE FUNCTION v3.editable_columns_for3(
    p_report_id INT,
    p_user_id   INT
)
RETURNS TABLE (
    column_key TEXT,
    closes_at  TIMESTAMPTZ
)
AS $function$
DECLARE
    v_role VARCHAR;
BEGIN
    IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
        RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
    END IF;

    v_role := v3.user_role_code(p_user_id);
    IF v_role IS NULL THEN
        RETURN;          -- неизвестный/неактивный юзер → пустая маска
    END IF;

    -- ADMIN: UNION всех column_keys всех фаз отчёта.
    IF v_role = 'ADMIN' THEN
        RETURN QUERY
        SELECT ck                 AS column_key,
               MAX(fp.closes_at)  AS closes_at
          FROM v3.form3_phase fp,
               LATERAL unnest(fp.column_keys) AS ck
         WHERE fp.rf_project_report_id = p_report_id
         GROUP BY ck;
        RETURN;
    END IF;

    -- EXECUTOR_RF без назначения → пустая маска (default deny).
    IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN
        RETURN;
    END IF;

    RETURN QUERY
    SELECT ck                        AS column_key,
           MAX(fp.closes_at)         AS closes_at
      FROM v3.form3_phase fp,
           LATERAL unnest(fp.column_keys) AS ck
     WHERE fp.rf_project_report_id = p_report_id
       AND fp.role                 = v_role
       AND now() BETWEEN fp.opens_at AND fp.closes_at
     GROUP BY ck;
END;
$function$ 
LANGUAGE plpgsql STABLE;

CREATE OR REPLACE FUNCTION v3.can_edit3(
    p_report_id  INT,
    p_column_key TEXT,
    p_user_id    INT
)
RETURNS JSONB
AS $function$
DECLARE
    v_role         VARCHAR;
    v_active_close TIMESTAMPTZ;
    v_role_has_col BOOLEAN;
    v_next_open    TIMESTAMPTZ;
    v_last_close   TIMESTAMPTZ;
BEGIN
    IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
        RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
    END IF;

    v_role := v3.user_role_code(p_user_id);
    IF v_role IS NULL THEN
        RETURN jsonb_build_object(
            'ok',     false,
            'code',   'role_not_allowed',
            'detail', 'unknown or inactive user'
        );
    END IF;

    -- ADMIN — bypass всех проверок.
    IF v_role = 'ADMIN' THEN
        RETURN jsonb_build_object('ok', true, 'admin', true);
    END IF;

    -- EXECUTOR_RF: фильтр по user_org проекта отчёта.
    IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN
        RETURN jsonb_build_object(
            'ok',     false,
            'code',   'org_not_assigned',
            'detail', p_column_key
        );
    END IF;

    -- 1. Активная сейчас фаза, в которой эта колонка перечислена для этой роли?
    SELECT MAX(fp.closes_at)
      INTO v_active_close
      FROM v3.form3_phase fp
     WHERE fp.rf_project_report_id = p_report_id
       AND fp.role                 = v_role
       AND p_column_key            = ANY(fp.column_keys)
       AND now() BETWEEN fp.opens_at AND fp.closes_at;

    IF v_active_close IS NOT NULL THEN
        RETURN jsonb_build_object(
            'ok',        true,
            'closes_at', v_active_close
        );
    END IF;

    -- 2. Колонка В ПРИНЦИПЕ есть хоть в одной фазе этой роли на этом отчёте?
    SELECT EXISTS (
        SELECT 1
          FROM v3.form3_phase fp
         WHERE fp.rf_project_report_id = p_report_id
           AND fp.role                 = v_role
           AND p_column_key            = ANY(fp.column_keys)
    ) INTO v_role_has_col;

    IF NOT v_role_has_col THEN
        RETURN jsonb_build_object(
            'ok',     false,
            'code',   'role_not_allowed',
            'detail', p_column_key
        );
    END IF;

    -- 3. Колонка доступна, но окно не активно. Подсказать ближайшее.
    SELECT MIN(fp.opens_at)
      INTO v_next_open
      FROM v3.form3_phase fp
     WHERE fp.rf_project_report_id = p_report_id
       AND fp.role                 = v_role
       AND p_column_key            = ANY(fp.column_keys)
       AND fp.opens_at             > now();

    IF v_next_open IS NOT NULL THEN
        RETURN jsonb_build_object(
            'ok',       false,
            'code',     'window_closed',
            'detail',   p_column_key,
            'opens_at', v_next_open
        );
    END IF;

    SELECT MAX(fp.closes_at)
      INTO v_last_close
      FROM v3.form3_phase fp
     WHERE fp.rf_project_report_id = p_report_id
       AND fp.role                 = v_role
       AND p_column_key            = ANY(fp.column_keys)
       AND fp.closes_at           <= now();

    RETURN jsonb_build_object(
        'ok',        false,
        'code',      'window_closed',
        'detail',    p_column_key,
        'closes_at', v_last_close
    );
END;
$function$ 
LANGUAGE plpgsql STABLE;

-- ─── 3. Authz-обёртки upd_form3_cell/cells (доп. сигнатуры с p_user_id) ─────

CREATE OR REPLACE FUNCTION v3.upd_form3_cells(
    p_report_id INT,
    p_changes   JSONB,
    p_user_id   INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
    v_change   JSONB;
    v_column   TEXT;
    v_decision JSONB;
    v_msg      TEXT;
BEGIN
    IF p_user_id IS NULL THEN
        RAISE EXCEPTION 'role_not_allowed: <missing user_id>';
    END IF;

    IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN
        RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id;
    END IF;

    IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
        RAISE EXCEPTION 'p_changes must be a JSONB array';
    END IF;

    FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
    LOOP
        v_column := v_change->>'column';
        IF v_column IS NULL THEN
            RAISE EXCEPTION 'change must have column: %', v_change;
        END IF;

        v_decision := v3.can_edit3(p_report_id, v_column, p_user_id);

        IF NOT (v_decision->>'ok')::boolean THEN
            IF v_decision->>'code' = 'role_not_allowed' THEN
                RAISE EXCEPTION 'role_not_allowed: %', v_column;

            ELSIF v_decision->>'code' = 'org_not_assigned' THEN
                RAISE EXCEPTION 'org_not_assigned: %', v_column;

            ELSIF v_decision->>'code' = 'window_closed' THEN
                IF v_decision ? 'opens_at' THEN
                    v_msg := format('window_closed: %s (opens %s)',
                                    v_column, v_decision->>'opens_at');
                ELSIF v_decision->>'closes_at' IS NOT NULL THEN
                    v_msg := format('window_closed: %s (closed %s)',
                                    v_column, v_decision->>'closes_at');
                ELSE
                    v_msg := format('window_closed: %s (no phase)', v_column);
                END IF;
                RAISE EXCEPTION '%', v_msg;

            ELSE
                RAISE EXCEPTION '%: %', v_decision->>'code', v_column;
            END IF;
        END IF;
    END LOOP;

    RETURN QUERY
    SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes);
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

CREATE OR REPLACE FUNCTION v3.upd_form3_cell(
    p_report_id INT,
    p_line_id   INT,
    p_column    TEXT,
    p_value     JSONB,
    p_user_id   INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
    SELECT * FROM v3.upd_form3_cells(
        p_report_id,
        jsonb_build_array(jsonb_build_object(
            'line_id', p_line_id,
            'column',  p_column,
            'value',   p_value
        )),
        p_user_id
    );
$function$ 
LANGUAGE sql VOLATILE;

-- ─── 4. add_project — раскладка snapshot обоих отчётов ──────────────────────

CREATE OR REPLACE FUNCTION v3.add_project(
    p_name           VARCHAR,
    p_year           INT,
    p_org_unit_id    INT,
    p_level          VARCHAR DEFAULT 'project',
    p_parent_id      INT     DEFAULT NULL,
    p_project_type   VARCHAR DEFAULT NULL,
    p_vsp_format     VARCHAR DEFAULT NULL,
    p_placement_type VARCHAR DEFAULT NULL,
    p_object_address VARCHAR DEFAULT NULL,
    p_staff_count    INT     DEFAULT NULL,
    p_total_area     NUMERIC DEFAULT NULL
)
RETURNS TABLE (
    project_id                  INT,
    limit_report_id             INT,
    current_expenses_report_id  INT
)
AS $function$
DECLARE
  v_pid INT;
  v_lim INT;
  v_cur INT;
BEGIN
  IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
    RAISE EXCEPTION 'p_name обязателен';
  END IF;
  IF p_year IS NULL THEN
    RAISE EXCEPTION 'p_year обязателен';
  END IF;
  IF p_org_unit_id IS NULL THEN
    RAISE EXCEPTION 'p_org_unit_id обязателен';
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
    RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
  END IF;
  IF p_parent_id IS NOT NULL AND
     NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
    RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
  END IF;

  INSERT INTO v3.project (
    name, level, parent_id, org_unit_id,
    project_type, vsp_format, placement_type, object_address, staff_count, total_area
  ) VALUES (
    p_name, p_level, p_parent_id, p_org_unit_id,
    p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area
  ) RETURNING id INTO v_pid;

  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (v_pid, p_year, 'LIMIT')            RETURNING id INTO v_lim;
  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (v_pid, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur;

  -- Snapshot RBAC-фаз (иначе default deny — отчёт read-only).
  PERFORM v3.copy_template_to_report(v_lim);
  PERFORM v3.copy_template_to_report(v_cur);

  PERFORM v3.log_event(
    'PROJECT_CREATE', 'PROJECT_CREATE',
    jsonb_strip_nulls(jsonb_build_object(
      'project_id',     v_pid,
      'name',           p_name,
      'year',           p_year,
      'level',          p_level,
      'parent_id',      p_parent_id,
      'org_unit_id',    p_org_unit_id,
      'project_type',   p_project_type,
      'vsp_format',     p_vsp_format,
      'placement_type', p_placement_type,
      'object_address', p_object_address,
      'staff_count',    p_staff_count,
      'total_area',     p_total_area,
      'limit_report_id',            v_lim,
      'current_expenses_report_id', v_cur
    ))
  );

  RETURN QUERY SELECT v_pid, v_lim, v_cur;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

