2603 lines
122 KiB
PL/PgSQL
2603 lines
122 KiB
PL/PgSQL
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.upd_form3_cell / v3.upd_form3_cells — точечная правка ячейки FORM_3.
|
||
-- Хранение FORM_3 идёт через rf_project_report → rf_project_report_line →
|
||
-- rf_project_report_quarter (PK = (rf_project_report_line_id, quarter)).
|
||
-- API симметричен upd_form_cell, но идентификатор «строки» — это
|
||
-- rf_project_report_line.id (то же `line_id`, что в JSONB read-функции).
|
||
--
|
||
-- Поддерживаемые ключи p_column (см. v_form3_report_jsonb.sql):
|
||
-- Факт: 'qN.adj_by_items', 'qN.adj_increase', 'qN.m1', 'qN.m2', 'qN.m3'
|
||
-- (+ 'q4.spod' — только IV квартал)
|
||
-- Базовый план: 'q1.base_plan' … 'q4.base_plan' (все 4 квартала)
|
||
-- Скоррект план: 'q2.corrected_plan' … 'q4.corrected_plan'
|
||
-- (без q1 — шаблон рисует скорр.план с II квартала)
|
||
--
|
||
-- Computed (RO): qN.total_corr, qN.quarter_actual, qN.economy (=«Остаток»),
|
||
-- qN.carryover, base_plan.year / totals.* (годовые итоги считаются на лету).
|
||
--
|
||
-- Атрибуты rf_project_report (dev_type, vsp_format, object_address, ...) —
|
||
-- НЕ через эту функцию (это header отчёта, не ячейка строки).
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
DROP FUNCTION IF EXISTS v3._apply_form3_cell(INT, TEXT, JSONB);
|
||
|
||
CREATE FUNCTION v3._apply_form3_cell(
|
||
p_line_id INT, -- rf_project_report_line.id
|
||
p_column TEXT,
|
||
p_value JSONB
|
||
)
|
||
RETURNS VOID
|
||
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';
|
||
WHEN 'base_plan' THEN v_target_col := 'base_plan'; v_target_type := 'NUMERIC';
|
||
WHEN 'corrected_plan' THEN
|
||
IF v_q = 1 THEN RAISE EXCEPTION 'corrected_plan is q2-q4 only: %', p_column; END IF;
|
||
v_target_col := 'corrected_plan'; 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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
|
||
-- ─── Public batch ──────────────────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB);
|
||
|
||
CREATE FUNCTION v3.upd_form3_cells(
|
||
p_report_id INT,
|
||
p_changes JSONB
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── Public single-cell wrapper ────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB);
|
||
|
||
CREATE FUNCTION v3.upd_form3_cell(
|
||
p_report_id INT,
|
||
p_line_id INT,
|
||
p_column TEXT,
|
||
p_value JSONB
|
||
)
|
||
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
|
||
))
|
||
);
|
||
$function$
|
||
LANGUAGE sql VOLATILE;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- FORM_3 write API: project header + line ADD/DEL + project ADD-master
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Дополняет существующий v3.upd_form3_cell (правка ячеек квартальных
|
||
-- значений в rf_project_report_quarter) — здесь функции для:
|
||
-- 1) v3.upd_project — правка атрибутов карточки проекта
|
||
-- 2) v3.add_form3_line — [DEPRECATED в UI] добавить позицию вручную.
|
||
-- 3) v3.del_form3_line — [DEPRECATED в UI] удалить позицию.
|
||
-- В FORM_3 сетка статей фиксирована (нет макроса добавления строк) —
|
||
-- строки засеваются автоматически, юзер пишет прямо в строку статьи.
|
||
-- add/del_form3_line оставлены для админ-правок/legacy.
|
||
-- 3b) v3.seed_form3_report_lines — засев полного набора листовых статей FORM_3.
|
||
-- 3c) v3.add_project_year — динамическое дорастание проекта годом
|
||
-- (2 отчёта + snapshot фаз + сетка статей).
|
||
-- 4) v3.add_project — мастер: project + первый год (add_project_year).
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
-- ─── 1. upd_project ────────────────────────────────────────────────────────
|
||
|
||
DROP FUNCTION IF EXISTS v3.upd_project(INT, TEXT, JSONB);
|
||
|
||
CREATE FUNCTION v3.upd_project(
|
||
p_project_id INT,
|
||
p_column TEXT,
|
||
p_value JSONB
|
||
)
|
||
RETURNS JSONB -- весь header проекта после правки
|
||
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 'system_code' THEN v_target_col := 'system_code'; 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,
|
||
'system_code', p.system_code,
|
||
'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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 2. add_form3_line ─────────────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.add_form3_line(INT, INT);
|
||
|
||
CREATE FUNCTION v3.add_form3_line(
|
||
p_report_id INT,
|
||
p_expense_item_id INT
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
AS $function$
|
||
DECLARE
|
||
v_new_id INT;
|
||
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;
|
||
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;
|
||
|
||
-- Аудит: ROW_CREATE
|
||
PERFORM v3.log_event(
|
||
'ROW_CREATE', 'ROW_CREATE',
|
||
jsonb_build_object(
|
||
'sheet', 'FORM_3',
|
||
'line_id', v_new_id,
|
||
'report_id', p_report_id,
|
||
'expense_item_id', p_expense_item_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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 3. del_form3_line ─────────────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.del_form3_line(INT);
|
||
|
||
CREATE FUNCTION v3.del_form3_line(
|
||
p_line_id INT
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
AS $function$
|
||
DECLARE
|
||
v_report_id INT;
|
||
v_eid 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]);
|
||
|
||
-- Каскад (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_DELETE',
|
||
jsonb_build_object(
|
||
'sheet', 'FORM_3',
|
||
'line_id', p_line_id,
|
||
'report_id', v_report_id,
|
||
'expense_item_id', v_eid
|
||
)
|
||
);
|
||
|
||
-- Возврат: только иерархия по пути удалённой (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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 3b. seed_form3_report_lines (фиксированная сетка статей) ──────────────
|
||
-- В FORM_3 строки НЕ добавляются пользователем (в шаблоне нет макроса добавления
|
||
-- строк) — сетка статей фиксирована, юзер пишет прямо в строку статьи. Поэтому
|
||
-- при создании отчёта засеваем ПОЛНЫЙ набор листовых статей FORM_3 (листовая =
|
||
-- FORM_3-статья без FORM_3-детей). Идемпотентно (uq_v3_rf_project_report_line).
|
||
-- Возвращает число вставленных строк.
|
||
DROP FUNCTION IF EXISTS v3.seed_form3_report_lines(INT);
|
||
|
||
CREATE FUNCTION v3.seed_form3_report_lines(p_report_id INT)
|
||
RETURNS INT
|
||
AS $function$
|
||
DECLARE
|
||
v_cnt INT;
|
||
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;
|
||
|
||
INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
|
||
SELECT p_report_id, e.id
|
||
FROM v3.expense_item e
|
||
JOIN v3.expense_item_form_type eft
|
||
ON eft.expense_item_id = e.id AND eft.form_type_code = 'FORM_3'
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM v3.expense_item c
|
||
JOIN v3.expense_item_form_type eftc
|
||
ON eftc.expense_item_id = c.id AND eftc.form_type_code = 'FORM_3'
|
||
WHERE c.parent_id = e.id
|
||
)
|
||
ON CONFLICT (rf_project_report_id, expense_item_id) DO NOTHING;
|
||
|
||
GET DIAGNOSTICS v_cnt = ROW_COUNT;
|
||
RETURN v_cnt;
|
||
END;
|
||
$function$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 3c. add_project_year (динамическое дорастание проекта по годам) ───────
|
||
-- Проект может дополняться годами в процессе работы. Заводит пару отчётов
|
||
-- (LIMIT + CURRENT_EXPENSES) на (project, year), снапшотит RBAC-фазы и засевает
|
||
-- фиксированную сетку статей. Год несёт report_id — column_keys без префикса
|
||
-- года, RBAC работает пофазно на каждый год-отчёт (см. seed_phase_template).
|
||
DROP FUNCTION IF EXISTS v3.add_project_year(INT, INT, BOOLEAN);
|
||
|
||
CREATE FUNCTION v3.add_project_year(
|
||
p_project_id INT,
|
||
p_year INT,
|
||
p_emit_audit BOOLEAN DEFAULT true -- false когда зовётся из add_project (там PROJECT_CREATE)
|
||
)
|
||
RETURNS TABLE (
|
||
limit_report_id INT,
|
||
current_expenses_report_id INT
|
||
)
|
||
AS $function$
|
||
DECLARE
|
||
v_lim INT;
|
||
v_cur INT;
|
||
BEGIN
|
||
IF p_year IS NULL THEN
|
||
RAISE EXCEPTION 'p_year обязателен';
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
|
||
RAISE EXCEPTION 'project id=% не существует', p_project_id;
|
||
END IF;
|
||
IF EXISTS (SELECT 1 FROM v3.rf_project_report
|
||
WHERE project_id = p_project_id AND year = p_year) THEN
|
||
RAISE EXCEPTION 'год % у проекта id=% уже заведён', p_year, p_project_id;
|
||
END IF;
|
||
|
||
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||
VALUES (p_project_id, p_year, 'LIMIT') RETURNING id INTO v_lim;
|
||
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||
VALUES (p_project_id, 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.seed_form3_report_lines(v_lim);
|
||
PERFORM v3.seed_form3_report_lines(v_cur);
|
||
|
||
IF p_emit_audit THEN
|
||
PERFORM v3.log_event(
|
||
'PROJECT_YEAR_ADD', 'PROJECT_YEAR_ADD',
|
||
jsonb_build_object(
|
||
'project_id', p_project_id,
|
||
'year', p_year,
|
||
'limit_report_id', v_lim,
|
||
'current_expenses_report_id', v_cur
|
||
)
|
||
);
|
||
END IF;
|
||
|
||
RETURN QUERY SELECT v_lim, v_cur;
|
||
END;
|
||
$function$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 4. add_project (мастер: project + первый год через add_project_year) ──
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC, VARCHAR);
|
||
|
||
CREATE FUNCTION v3.add_project(
|
||
p_name VARCHAR,
|
||
p_year INT,
|
||
p_org_unit_id INT,
|
||
p_level VARCHAR DEFAULT 'project', -- 'program' | 'project'
|
||
p_parent_id INT DEFAULT NULL, -- обязателен если level='project' и есть программа
|
||
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,
|
||
p_system_code VARCHAR 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 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,
|
||
system_code
|
||
) 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,
|
||
p_system_code
|
||
) RETURNING id INTO v_pid;
|
||
|
||
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
||
-- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
|
||
-- годы добавляются отдельно через v3.add_project_year.
|
||
SELECT y.limit_report_id, y.current_expenses_report_id
|
||
INTO v_lim, v_cur
|
||
FROM v3.add_project_year(v_pid, p_year, false) y;
|
||
|
||
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
|
||
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,
|
||
'system_code', p_system_code,
|
||
'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;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.v_form3_report_sections — отображение листа ЛП (LIMIT) или ТР (CURRENT)
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Возвращает иерархию (ROOT/GROUP/ITEM/SUB_ITEM) + INPUT строки в tree-order
|
||
-- для одного rf_project_report (один трек: report_type = LIMIT либо
|
||
-- CURRENT_EXPENSES). UI выбирает трек, передавая report_id соответствующей
|
||
-- записи rf_project_report.
|
||
--
|
||
-- Колоночная раскладка повторяет шаблон Формы 3:
|
||
-- header, q1, q2, q3, q4, year_totals.
|
||
--
|
||
-- Q1: adj_by_items, adj_increase, total_corr, m1, m2, m3, quarter_actual, economy
|
||
-- Q2/Q3: carryover, adj_by_items, adj_increase, total_corr, m1..m3, quarter_actual, economy
|
||
-- Q4: carryover, adj_by_items, adj_increase, total_corr, m1..m3, spod, quarter_actual, economy
|
||
-- year_totals: total_year_corr, total_year_actual, economy_year
|
||
--
|
||
-- p_sections — массив включаемых блоков (NULL = все). Допустимые имена:
|
||
-- 'q1', 'q2', 'q3', 'q4', 'year'
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
DROP FUNCTION IF EXISTS v3.v_form3_report_sections(INT, TEXT[]);
|
||
|
||
CREATE FUNCTION v3.v_form3_report_sections(
|
||
p_report_id INT,
|
||
p_sections TEXT[] DEFAULT NULL
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
line_id INT,
|
||
col_section_code VARCHAR,
|
||
col_item_id VARCHAR,
|
||
col_num_group_id VARCHAR,
|
||
col_name VARCHAR,
|
||
-- Q1
|
||
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,
|
||
-- Q2
|
||
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,
|
||
-- Q3
|
||
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,
|
||
-- Q4
|
||
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,
|
||
-- year totals
|
||
col_year_total_corr NUMERIC,
|
||
col_year_total_actual NUMERIC,
|
||
col_year_economy NUMERIC,
|
||
-- план-блоки (v2): Базовый план (q1–q4 + Год) и Скоррект план (q2–q4)
|
||
col_q1_base_plan NUMERIC,
|
||
col_q2_base_plan NUMERIC,
|
||
col_q3_base_plan NUMERIC,
|
||
col_q4_base_plan NUMERIC,
|
||
col_year_base_plan NUMERIC,
|
||
col_q2_corrected_plan NUMERIC,
|
||
col_q3_corrected_plan NUMERIC,
|
||
col_q4_corrected_plan NUMERIC,
|
||
_sort_path INT[]
|
||
)
|
||
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,
|
||
q.base_plan, q.corrected_plan
|
||
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,
|
||
q1.base_plan AS q1_bp, q2.base_plan AS q2_bp,
|
||
q3.base_plan AS q3_bp, q4.base_plan AS q4_bp,
|
||
q2.corrected_plan AS q2_cp, q3.corrected_plan AS q3_cp, q4.corrected_plan AS q4_cp
|
||
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,
|
||
SUM(q.base_plan) AS bp, SUM(q.corrected_plan) AS cp
|
||
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,
|
||
SUM(a.bp) AS bp
|
||
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,
|
||
SUM(a.bp) AS bp, SUM(a.cp) AS cp
|
||
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,
|
||
SUM(a.bp) AS bp, SUM(a.cp) AS cp
|
||
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,
|
||
SUM(a.bp) AS bp, SUM(a.cp) AS cp
|
||
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,
|
||
-- план-блоки
|
||
CASE WHEN s_q1 THEN e.q1_bp END,
|
||
CASE WHEN s_q2 THEN e.q2_bp END,
|
||
CASE WHEN s_q3 THEN e.q3_bp END,
|
||
CASE WHEN s_q4 THEN e.q4_bp END,
|
||
CASE WHEN s_year THEN COALESCE(e.q1_bp,0)+COALESCE(e.q2_bp,0)+
|
||
COALESCE(e.q3_bp,0)+COALESCE(e.q4_bp,0) END,
|
||
CASE WHEN s_q2 THEN e.q2_cp END,
|
||
CASE WHEN s_q3 THEN e.q3_cp END,
|
||
CASE WHEN s_q4 THEN e.q4_cp 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,
|
||
-- план-блоки (rollup)
|
||
CASE WHEN s_q1 THEN a1.bp END,
|
||
CASE WHEN s_q2 THEN a2.bp END,
|
||
CASE WHEN s_q3 THEN a3.bp END,
|
||
CASE WHEN s_q4 THEN a4.bp END,
|
||
CASE WHEN s_year THEN COALESCE(a1.bp,0)+COALESCE(a2.bp,0)+
|
||
COALESCE(a3.bp,0)+COALESCE(a4.bp,0) END,
|
||
CASE WHEN s_q2 THEN a2.cp END,
|
||
CASE WHEN s_q3 THEN a3.cp END,
|
||
CASE WHEN s_q4 THEN a4.cp 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$
|
||
LANGUAGE plpgsql STABLE;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.upd_form3_cell / v3.upd_form3_cells — обёртки FORM_3 с проверкой роли+окна
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Параллель upd_form_cell_authz.sql, но для ветки rf_project_report. Каждая
|
||
-- ячейка прогоняется через v3.can_edit3(report_id, column, user_id). Если хоть
|
||
-- одна запрещена — RAISE EXCEPTION в формате, совместимом с конвенцией v3:
|
||
-- • role_not_allowed: <column>
|
||
-- • org_not_assigned: <column>
|
||
-- • window_closed: <column> (opens <ISO8601>) — окно ещё не наступило
|
||
-- • window_closed: <column> (closed <ISO8601>) — окно уже закрыто
|
||
-- • window_closed: <column> (no phase) — фаз вообще нет
|
||
--
|
||
-- HTTP-слой парсит префикс до `:` и мапит на 403 (см. SPEC.md §5).
|
||
--
|
||
-- ⚠ Применять СТРОГО после upd_form3_cell.sql (low-level 2-арг/4-арг). Здесь
|
||
-- добавляется p_user_id → это ДРУГИЕ сигнатуры (3-арг/5-арг), которые
|
||
-- сосуществуют с low-level и делегируют в них после проверки. HTTP вызывает
|
||
-- версии с user_id. «Листа» тут нет — report_type определяется отчётом внутри
|
||
-- can_edit3.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
-- ─── upd_form3_cells (массовый, с авторизацией) ────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB, INT);
|
||
|
||
CREATE 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;
|
||
|
||
-- Все ячейки разрешены — делегируем в low-level (2-арг). ROW_UPDATE не
|
||
-- логируем (см. logging_events.md): cell-edit'ы слишком частые.
|
||
RETURN QUERY
|
||
SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes);
|
||
END;
|
||
$function$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── upd_form3_cell (одна ячейка, тонкий wrapper) ──────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB, INT);
|
||
|
||
CREATE 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;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.migrate_form3_v2 — DDL под переработку FORM_3 (листы ЛП/ТР, v2)
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Источник: «Пример_Форма_3_new(1).xlsm» (листы шаблон_ЛП / шаблон_ТР) + PDF-дифф
|
||
-- аналитика. Финансовая часть переразбита с 4 на 10 блоков и переведена на
|
||
-- многолетнюю раскладку (год1/год2/год3 рядом, кварталы повторяются на год).
|
||
--
|
||
-- Новые блоки внутри одного года:
|
||
-- • Базовый план по Лимиту Правления — ручной ввод q1–q4, Год = Σ кв.
|
||
-- • Скоррект план по Лимиту Правления — ручной ввод q2–q4 (без q1, без Года)
|
||
-- • Фактические затраты — уже покрыты rf_project_report_quarter
|
||
-- (adj_by_items / adj_increase / actual_m1..m3 / actual_spod), «Остаток» и
|
||
-- квартальные/годовые итоги считаются на лету в read-функции.
|
||
--
|
||
-- Многолетность уже поддержана моделью: один project → много rf_project_report
|
||
-- по (year × report_type). Здесь добавляется ТОЛЬКО хранение план-блоков и
|
||
-- «Системный код ВСП» в шапке проекта.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
-- ───────────── 1. rf_project_report_quarter: план-блоки ─────────────────────
|
||
-- base_plan — «Базовый план по Лимиту Правления», поквартально (q1–q4).
|
||
-- corrected_plan — «Скоррект план по Лимиту Правления», поквартально (q2–q4).
|
||
-- Для q1 значения не существует (шаблон рисует скорр.план с
|
||
-- II квартала) — гарантируется CHECK ниже + write-слой/RBAC.
|
||
ALTER TABLE v3.rf_project_report_quarter
|
||
ADD COLUMN IF NOT EXISTS base_plan NUMERIC,
|
||
ADD COLUMN IF NOT EXISTS corrected_plan NUMERIC;
|
||
|
||
-- Скоррект план не заводится в I квартале (нет колонки в шаблоне).
|
||
ALTER TABLE v3.rf_project_report_quarter
|
||
DROP CONSTRAINT IF EXISTS chk_v3_rf_quarter_corr_plan_q1;
|
||
ALTER TABLE v3.rf_project_report_quarter
|
||
ADD CONSTRAINT chk_v3_rf_quarter_corr_plan_q1
|
||
CHECK (quarter <> 1 OR corrected_plan IS NULL);
|
||
|
||
-- ───────────── 2. v3.project: «Системный код ВСП» ───────────────────────────
|
||
-- Новое поле шапки «Информация о проекте ВСП» (в примере — 1808). Выбор из
|
||
-- справочника ИНФО / выпадающего списка; хранится как атрибут проекта (вводится
|
||
-- один раз, не дублируется между треками/годами — как и остальные атрибуты ВСП).
|
||
ALTER TABLE v3.project
|
||
ADD COLUMN IF NOT EXISTS system_code VARCHAR;
|
||
|
||
-- ───────────── 3. Обновление энум-списков шапки ────────────────────────────
|
||
-- В новом шаблоне списки изменились относительно старого VBA-набора.
|
||
-- project_type: новый перечень из 8 значений (шаблон_ЛП F2:F9).
|
||
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_project_type_check;
|
||
ALTER TABLE v3.project
|
||
ADD CONSTRAINT project_project_type_check
|
||
CHECK (project_type IS NULL OR project_type IN (
|
||
'Открытие ВСП','Реновация ВСП','Закрытие ВСП',
|
||
'Реновация РФ','Закрытие РФ',
|
||
'Открытие УРМ','Реновация УРМ','Закрытие УРМ'
|
||
));
|
||
|
||
-- vsp_format («Новый формат ВСП») — поле автозаполняется из справочника INFO
|
||
-- («Утвержденный формат»), не вводится вручную и его словарь ведётся как
|
||
-- reference-data. Жёсткий CHECK снимаем, чтобы новые форматы не ломали вставку.
|
||
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_vsp_format_check;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.upd_form3_cell / v3.upd_form3_cells — точечная правка ячейки FORM_3.
|
||
-- Хранение FORM_3 идёт через rf_project_report → rf_project_report_line →
|
||
-- rf_project_report_quarter (PK = (rf_project_report_line_id, quarter)).
|
||
-- API симметричен upd_form_cell, но идентификатор «строки» — это
|
||
-- rf_project_report_line.id (то же `line_id`, что в JSONB read-функции).
|
||
--
|
||
-- Поддерживаемые ключи p_column (см. v_form3_report_jsonb.sql):
|
||
-- Факт: 'qN.adj_by_items', 'qN.adj_increase', 'qN.m1', 'qN.m2', 'qN.m3'
|
||
-- (+ 'q4.spod' — только IV квартал)
|
||
-- Базовый план: 'q1.base_plan' … 'q4.base_plan' (все 4 квартала)
|
||
-- Скоррект план: 'q2.corrected_plan' … 'q4.corrected_plan'
|
||
-- (без q1 — шаблон рисует скорр.план с II квартала)
|
||
--
|
||
-- Computed (RO): qN.total_corr, qN.quarter_actual, qN.economy (=«Остаток»),
|
||
-- qN.carryover, base_plan.year / totals.* (годовые итоги считаются на лету).
|
||
--
|
||
-- Атрибуты rf_project_report (dev_type, vsp_format, object_address, ...) —
|
||
-- НЕ через эту функцию (это header отчёта, не ячейка строки).
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
DROP FUNCTION IF EXISTS v3._apply_form3_cell(INT, TEXT, JSONB);
|
||
|
||
CREATE FUNCTION v3._apply_form3_cell(
|
||
p_line_id INT, -- rf_project_report_line.id
|
||
p_column TEXT,
|
||
p_value JSONB
|
||
)
|
||
RETURNS VOID
|
||
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';
|
||
WHEN 'base_plan' THEN v_target_col := 'base_plan'; v_target_type := 'NUMERIC';
|
||
WHEN 'corrected_plan' THEN
|
||
IF v_q = 1 THEN RAISE EXCEPTION 'corrected_plan is q2-q4 only: %', p_column; END IF;
|
||
v_target_col := 'corrected_plan'; 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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── Public batch ──────────────────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB);
|
||
|
||
CREATE FUNCTION v3.upd_form3_cells(
|
||
p_report_id INT,
|
||
p_changes JSONB
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── Public single-cell wrapper ────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB);
|
||
|
||
CREATE FUNCTION v3.upd_form3_cell(
|
||
p_report_id INT,
|
||
p_line_id INT,
|
||
p_column TEXT,
|
||
p_value JSONB
|
||
)
|
||
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
|
||
))
|
||
);
|
||
$function$
|
||
LANGUAGE sql VOLATILE;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- FORM_3 write API: project header + line ADD/DEL + project ADD-master
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Дополняет существующий v3.upd_form3_cell (правка ячеек квартальных
|
||
-- значений в rf_project_report_quarter) — здесь функции для:
|
||
-- 1) v3.upd_project — правка атрибутов карточки проекта
|
||
-- 2) v3.add_form3_line — [DEPRECATED в UI] добавить позицию вручную.
|
||
-- 3) v3.del_form3_line — [DEPRECATED в UI] удалить позицию.
|
||
-- В FORM_3 сетка статей фиксирована (нет макроса добавления строк) —
|
||
-- строки засеваются автоматически, юзер пишет прямо в строку статьи.
|
||
-- add/del_form3_line оставлены для админ-правок/legacy.
|
||
-- 3b) v3.seed_form3_report_lines — засев полного набора листовых статей FORM_3.
|
||
-- 3c) v3.add_project_year — динамическое дорастание проекта годом
|
||
-- (2 отчёта + snapshot фаз + сетка статей).
|
||
-- 4) v3.add_project — мастер: project + первый год (add_project_year).
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
-- ─── 1. upd_project ────────────────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_project(INT, TEXT, JSONB);
|
||
|
||
CREATE FUNCTION v3.upd_project(
|
||
p_project_id INT,
|
||
p_column TEXT,
|
||
p_value JSONB
|
||
)
|
||
RETURNS JSONB -- весь header проекта после правки
|
||
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 'system_code' THEN v_target_col := 'system_code'; 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,
|
||
'system_code', p.system_code,
|
||
'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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 2. add_form3_line ─────────────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.add_form3_line(INT, INT);
|
||
|
||
CREATE FUNCTION v3.add_form3_line(
|
||
p_report_id INT,
|
||
p_expense_item_id INT
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
AS $function$
|
||
DECLARE
|
||
v_new_id INT;
|
||
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;
|
||
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;
|
||
|
||
-- Аудит: ROW_CREATE
|
||
PERFORM v3.log_event(
|
||
'ROW_CREATE', 'ROW_CREATE',
|
||
jsonb_build_object(
|
||
'sheet', 'FORM_3',
|
||
'line_id', v_new_id,
|
||
'report_id', p_report_id,
|
||
'expense_item_id', p_expense_item_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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 3. del_form3_line ─────────────────────────────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.del_form3_line(INT);
|
||
|
||
CREATE FUNCTION v3.del_form3_line(
|
||
p_line_id INT
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
AS $function$
|
||
DECLARE
|
||
v_report_id INT;
|
||
v_eid 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]);
|
||
|
||
-- Каскад (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_DELETE',
|
||
jsonb_build_object(
|
||
'sheet', 'FORM_3',
|
||
'line_id', p_line_id,
|
||
'report_id', v_report_id,
|
||
'expense_item_id', v_eid
|
||
)
|
||
);
|
||
|
||
-- Возврат: только иерархия по пути удалённой (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$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 3b. seed_form3_report_lines (фиксированная сетка статей) ──────────────
|
||
-- В FORM_3 строки НЕ добавляются пользователем (в шаблоне нет макроса добавления
|
||
-- строк) — сетка статей фиксирована, юзер пишет прямо в строку статьи. Поэтому
|
||
-- при создании отчёта засеваем ПОЛНЫЙ набор листовых статей FORM_3 (листовая =
|
||
-- FORM_3-статья без FORM_3-детей). Идемпотентно (uq_v3_rf_project_report_line).
|
||
-- Возвращает число вставленных строк.
|
||
DROP FUNCTION IF EXISTS v3.seed_form3_report_lines(INT);
|
||
|
||
CREATE FUNCTION v3.seed_form3_report_lines(p_report_id INT)
|
||
RETURNS INT
|
||
AS $function$
|
||
DECLARE
|
||
v_cnt INT;
|
||
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;
|
||
|
||
INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
|
||
SELECT p_report_id, e.id
|
||
FROM v3.expense_item e
|
||
JOIN v3.expense_item_form_type eft
|
||
ON eft.expense_item_id = e.id AND eft.form_type_code = 'FORM_3'
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM v3.expense_item c
|
||
JOIN v3.expense_item_form_type eftc
|
||
ON eftc.expense_item_id = c.id AND eftc.form_type_code = 'FORM_3'
|
||
WHERE c.parent_id = e.id
|
||
)
|
||
ON CONFLICT (rf_project_report_id, expense_item_id) DO NOTHING;
|
||
|
||
GET DIAGNOSTICS v_cnt = ROW_COUNT;
|
||
RETURN v_cnt;
|
||
END;
|
||
$function$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 3c. add_project_year (динамическое дорастание проекта по годам) ───────
|
||
-- Проект может дополняться годами в процессе работы. Заводит пару отчётов
|
||
-- (LIMIT + CURRENT_EXPENSES) на (project, year), снапшотит RBAC-фазы и засевает
|
||
-- фиксированную сетку статей. Год несёт report_id — column_keys без префикса
|
||
-- года, RBAC работает пофазно на каждый год-отчёт (см. seed_phase_template).
|
||
DROP FUNCTION IF EXISTS v3.add_project_year(INT, INT, BOOLEAN);
|
||
|
||
CREATE FUNCTION v3.add_project_year(
|
||
p_project_id INT,
|
||
p_year INT,
|
||
p_emit_audit BOOLEAN DEFAULT true -- false когда зовётся из add_project (там PROJECT_CREATE)
|
||
)
|
||
RETURNS TABLE (
|
||
limit_report_id INT,
|
||
current_expenses_report_id INT
|
||
)
|
||
AS $function$
|
||
DECLARE
|
||
v_lim INT;
|
||
v_cur INT;
|
||
BEGIN
|
||
IF p_year IS NULL THEN
|
||
RAISE EXCEPTION 'p_year обязателен';
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
|
||
RAISE EXCEPTION 'project id=% не существует', p_project_id;
|
||
END IF;
|
||
IF EXISTS (SELECT 1 FROM v3.rf_project_report
|
||
WHERE project_id = p_project_id AND year = p_year) THEN
|
||
RAISE EXCEPTION 'год % у проекта id=% уже заведён', p_year, p_project_id;
|
||
END IF;
|
||
|
||
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||
VALUES (p_project_id, p_year, 'LIMIT') RETURNING id INTO v_lim;
|
||
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||
VALUES (p_project_id, 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.seed_form3_report_lines(v_lim);
|
||
PERFORM v3.seed_form3_report_lines(v_cur);
|
||
|
||
IF p_emit_audit THEN
|
||
PERFORM v3.log_event(
|
||
'PROJECT_YEAR_ADD', 'PROJECT_YEAR_ADD',
|
||
jsonb_build_object(
|
||
'project_id', p_project_id,
|
||
'year', p_year,
|
||
'limit_report_id', v_lim,
|
||
'current_expenses_report_id', v_cur
|
||
)
|
||
);
|
||
END IF;
|
||
|
||
RETURN QUERY SELECT v_lim, v_cur;
|
||
END;
|
||
$function$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── 4. add_project (мастер: project + первый год через add_project_year) ──
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
|
||
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC, VARCHAR);
|
||
|
||
CREATE FUNCTION v3.add_project(
|
||
p_name VARCHAR,
|
||
p_year INT,
|
||
p_org_unit_id INT,
|
||
p_level VARCHAR DEFAULT 'project', -- 'program' | 'project'
|
||
p_parent_id INT DEFAULT NULL, -- обязателен если level='project' и есть программа
|
||
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,
|
||
p_system_code VARCHAR 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 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,
|
||
system_code
|
||
) 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,
|
||
p_system_code
|
||
) RETURNING id INTO v_pid;
|
||
|
||
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
||
-- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
|
||
-- годы добавляются отдельно через v3.add_project_year.
|
||
SELECT y.limit_report_id, y.current_expenses_report_id
|
||
INTO v_lim, v_cur
|
||
FROM v3.add_project_year(v_pid, p_year, false) y;
|
||
|
||
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
|
||
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,
|
||
'system_code', p_system_code,
|
||
'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;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.v_form3_report_sections — отображение листа ЛП (LIMIT) или ТР (CURRENT)
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Возвращает иерархию (ROOT/GROUP/ITEM/SUB_ITEM) + INPUT строки в tree-order
|
||
-- для одного rf_project_report (один трек: report_type = LIMIT либо
|
||
-- CURRENT_EXPENSES). UI выбирает трек, передавая report_id соответствующей
|
||
-- записи rf_project_report.
|
||
--
|
||
-- Колоночная раскладка повторяет шаблон Формы 3:
|
||
-- header, q1, q2, q3, q4, year_totals.
|
||
--
|
||
-- Q1: adj_by_items, adj_increase, total_corr, m1, m2, m3, quarter_actual, economy
|
||
-- Q2/Q3: carryover, adj_by_items, adj_increase, total_corr, m1..m3, quarter_actual, economy
|
||
-- Q4: carryover, adj_by_items, adj_increase, total_corr, m1..m3, spod, quarter_actual, economy
|
||
-- year_totals: total_year_corr, total_year_actual, economy_year
|
||
--
|
||
-- p_sections — массив включаемых блоков (NULL = все). Допустимые имена:
|
||
-- 'q1', 'q2', 'q3', 'q4', 'year'
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
DROP FUNCTION IF EXISTS v3.v_form3_report_sections(INT, TEXT[]);
|
||
|
||
CREATE FUNCTION v3.v_form3_report_sections(
|
||
p_report_id INT,
|
||
p_sections TEXT[] DEFAULT NULL
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
line_id INT,
|
||
col_section_code VARCHAR,
|
||
col_item_id VARCHAR,
|
||
col_num_group_id VARCHAR,
|
||
col_name VARCHAR,
|
||
-- Q1
|
||
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,
|
||
-- Q2
|
||
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,
|
||
-- Q3
|
||
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,
|
||
-- Q4
|
||
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,
|
||
-- year totals
|
||
col_year_total_corr NUMERIC,
|
||
col_year_total_actual NUMERIC,
|
||
col_year_economy NUMERIC,
|
||
-- план-блоки (v2): Базовый план (q1–q4 + Год) и Скоррект план (q2–q4)
|
||
col_q1_base_plan NUMERIC,
|
||
col_q2_base_plan NUMERIC,
|
||
col_q3_base_plan NUMERIC,
|
||
col_q4_base_plan NUMERIC,
|
||
col_year_base_plan NUMERIC,
|
||
col_q2_corrected_plan NUMERIC,
|
||
col_q3_corrected_plan NUMERIC,
|
||
col_q4_corrected_plan NUMERIC,
|
||
_sort_path INT[]
|
||
)
|
||
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,
|
||
q.base_plan, q.corrected_plan
|
||
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,
|
||
q1.base_plan AS q1_bp, q2.base_plan AS q2_bp,
|
||
q3.base_plan AS q3_bp, q4.base_plan AS q4_bp,
|
||
q2.corrected_plan AS q2_cp, q3.corrected_plan AS q3_cp, q4.corrected_plan AS q4_cp
|
||
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,
|
||
SUM(q.base_plan) AS bp, SUM(q.corrected_plan) AS cp
|
||
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,
|
||
SUM(a.bp) AS bp
|
||
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,
|
||
SUM(a.bp) AS bp, SUM(a.cp) AS cp
|
||
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,
|
||
SUM(a.bp) AS bp, SUM(a.cp) AS cp
|
||
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,
|
||
SUM(a.bp) AS bp, SUM(a.cp) AS cp
|
||
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,
|
||
-- план-блоки
|
||
CASE WHEN s_q1 THEN e.q1_bp END,
|
||
CASE WHEN s_q2 THEN e.q2_bp END,
|
||
CASE WHEN s_q3 THEN e.q3_bp END,
|
||
CASE WHEN s_q4 THEN e.q4_bp END,
|
||
CASE WHEN s_year THEN COALESCE(e.q1_bp,0)+COALESCE(e.q2_bp,0)+
|
||
COALESCE(e.q3_bp,0)+COALESCE(e.q4_bp,0) END,
|
||
CASE WHEN s_q2 THEN e.q2_cp END,
|
||
CASE WHEN s_q3 THEN e.q3_cp END,
|
||
CASE WHEN s_q4 THEN e.q4_cp 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,
|
||
-- план-блоки (rollup)
|
||
CASE WHEN s_q1 THEN a1.bp END,
|
||
CASE WHEN s_q2 THEN a2.bp END,
|
||
CASE WHEN s_q3 THEN a3.bp END,
|
||
CASE WHEN s_q4 THEN a4.bp END,
|
||
CASE WHEN s_year THEN COALESCE(a1.bp,0)+COALESCE(a2.bp,0)+
|
||
COALESCE(a3.bp,0)+COALESCE(a4.bp,0) END,
|
||
CASE WHEN s_q2 THEN a2.cp END,
|
||
CASE WHEN s_q3 THEN a3.cp END,
|
||
CASE WHEN s_q4 THEN a4.cp 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$
|
||
LANGUAGE plpgsql STABLE;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.upd_form3_cell / v3.upd_form3_cells — обёртки FORM_3 с проверкой роли+окна
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Параллель upd_form_cell_authz.sql, но для ветки rf_project_report. Каждая
|
||
-- ячейка прогоняется через v3.can_edit3(report_id, column, user_id). Если хоть
|
||
-- одна запрещена — RAISE EXCEPTION в формате, совместимом с конвенцией v3:
|
||
-- • role_not_allowed: <column>
|
||
-- • org_not_assigned: <column>
|
||
-- • window_closed: <column> (opens <ISO8601>) — окно ещё не наступило
|
||
-- • window_closed: <column> (closed <ISO8601>) — окно уже закрыто
|
||
-- • window_closed: <column> (no phase) — фаз вообще нет
|
||
--
|
||
-- HTTP-слой парсит префикс до `:` и мапит на 403 (см. SPEC.md §5).
|
||
--
|
||
-- ⚠ Применять СТРОГО после upd_form3_cell.sql (low-level 2-арг/4-арг). Здесь
|
||
-- добавляется p_user_id → это ДРУГИЕ сигнатуры (3-арг/5-арг), которые
|
||
-- сосуществуют с low-level и делегируют в них после проверки. HTTP вызывает
|
||
-- версии с user_id. «Листа» тут нет — report_type определяется отчётом внутри
|
||
-- can_edit3.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
-- ─── upd_form3_cells (массовый, с авторизацией) ────────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB, INT);
|
||
|
||
CREATE 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;
|
||
|
||
-- Все ячейки разрешены — делегируем в low-level (2-арг). ROW_UPDATE не
|
||
-- логируем (см. logging_events.md): cell-edit'ы слишком частые.
|
||
RETURN QUERY
|
||
SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes);
|
||
END;
|
||
$function$
|
||
LANGUAGE plpgsql VOLATILE;
|
||
|
||
-- ─── upd_form3_cell (одна ячейка, тонкий wrapper) ──────────────────────────
|
||
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB, INT);
|
||
|
||
CREATE 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;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.migrate_form3_v2 — DDL под переработку FORM_3 (листы ЛП/ТР, v2)
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Источник: «Пример_Форма_3_new(1).xlsm» (листы шаблон_ЛП / шаблон_ТР) + PDF-дифф
|
||
-- аналитика. Финансовая часть переразбита с 4 на 10 блоков и переведена на
|
||
-- многолетнюю раскладку (год1/год2/год3 рядом, кварталы повторяются на год).
|
||
--
|
||
-- Новые блоки внутри одного года:
|
||
-- • Базовый план по Лимиту Правления — ручной ввод q1–q4, Год = Σ кв.
|
||
-- • Скоррект план по Лимиту Правления — ручной ввод q2–q4 (без q1, без Года)
|
||
-- • Фактические затраты — уже покрыты rf_project_report_quarter
|
||
-- (adj_by_items / adj_increase / actual_m1..m3 / actual_spod), «Остаток» и
|
||
-- квартальные/годовые итоги считаются на лету в read-функции.
|
||
--
|
||
-- Многолетность уже поддержана моделью: один project → много rf_project_report
|
||
-- по (year × report_type). Здесь добавляется ТОЛЬКО хранение план-блоков и
|
||
-- «Системный код ВСП» в шапке проекта.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
-- ───────────── 1. rf_project_report_quarter: план-блоки ─────────────────────
|
||
-- base_plan — «Базовый план по Лимиту Правления», поквартально (q1–q4).
|
||
-- corrected_plan — «Скоррект план по Лимиту Правления», поквартально (q2–q4).
|
||
-- Для q1 значения не существует (шаблон рисует скорр.план с
|
||
-- II квартала) — гарантируется CHECK ниже + write-слой/RBAC.
|
||
ALTER TABLE v3.rf_project_report_quarter
|
||
ADD COLUMN IF NOT EXISTS base_plan NUMERIC,
|
||
ADD COLUMN IF NOT EXISTS corrected_plan NUMERIC;
|
||
|
||
-- Скоррект план не заводится в I квартале (нет колонки в шаблоне).
|
||
ALTER TABLE v3.rf_project_report_quarter
|
||
DROP CONSTRAINT IF EXISTS chk_v3_rf_quarter_corr_plan_q1;
|
||
ALTER TABLE v3.rf_project_report_quarter
|
||
ADD CONSTRAINT chk_v3_rf_quarter_corr_plan_q1
|
||
CHECK (quarter <> 1 OR corrected_plan IS NULL);
|
||
|
||
-- ───────────── 2. v3.project: «Системный код ВСП» ───────────────────────────
|
||
-- Новое поле шапки «Информация о проекте ВСП» (в примере — 1808). Выбор из
|
||
-- справочника ИНФО / выпадающего списка; хранится как атрибут проекта (вводится
|
||
-- один раз, не дублируется между треками/годами — как и остальные атрибуты ВСП).
|
||
ALTER TABLE v3.project
|
||
ADD COLUMN IF NOT EXISTS system_code VARCHAR;
|
||
|
||
-- ───────────── 3. Обновление энум-списков шапки ────────────────────────────
|
||
-- В новом шаблоне списки изменились относительно старого VBA-набора.
|
||
-- project_type: новый перечень из 8 значений (шаблон_ЛП F2:F9).
|
||
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_project_type_check;
|
||
ALTER TABLE v3.project
|
||
ADD CONSTRAINT project_project_type_check
|
||
CHECK (project_type IS NULL OR project_type IN (
|
||
'Открытие ВСП','Реновация ВСП','Закрытие ВСП',
|
||
'Реновация РФ','Закрытие РФ',
|
||
'Открытие УРМ','Реновация УРМ','Закрытие УРМ'
|
||
));
|
||
|
||
-- vsp_format («Новый формат ВСП») — поле автозаполняется из справочника INFO
|
||
-- («Утвержденный формат»), не вводится вручную и его словарь ведётся как
|
||
-- reference-data. Жёсткий CHECK снимаем, чтобы новые форматы не ломали вставку.
|
||
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_vsp_format_check;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.v_form3_report_jsonb — JSONB-обёртка над v_form3_report_sections
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Один трек FORM_3 (LIMIT либо CURRENT_EXPENSES) как (row_type, depth,
|
||
-- sort_order, data jsonb) — общий формат UI с FORM_1/2/4.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
DROP FUNCTION IF EXISTS v3.v_form3_report_jsonb(INT, TEXT[]);
|
||
|
||
CREATE FUNCTION v3.v_form3_report_jsonb(
|
||
p_report_id INT,
|
||
p_sections TEXT[] DEFAULT NULL
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
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(
|
||
'base_plan', s.col_q1_base_plan,
|
||
'adj_by_items', s.col_q1_adj_by_items,
|
||
'adj_increase', s.col_q1_adj_increase,
|
||
'total_corr', s.col_q1_total_corr,
|
||
'm1', s.col_q1_m1,
|
||
'm2', s.col_q1_m2,
|
||
'm3', s.col_q1_m3,
|
||
'quarter_actual', s.col_q1_quarter_actual,
|
||
'economy', s.col_q1_economy -- «Остаток» = корр+увелич−квартал
|
||
),
|
||
'q2', jsonb_build_object(
|
||
'base_plan', s.col_q2_base_plan,
|
||
'corrected_plan', s.col_q2_corrected_plan,
|
||
'carryover', s.col_q2_carryover,
|
||
'adj_by_items', s.col_q2_adj_by_items,
|
||
'adj_increase', s.col_q2_adj_increase,
|
||
'total_corr', s.col_q2_total_corr,
|
||
'm1', s.col_q2_m1,
|
||
'm2', s.col_q2_m2,
|
||
'm3', s.col_q2_m3,
|
||
'quarter_actual', s.col_q2_quarter_actual,
|
||
'economy', s.col_q2_economy
|
||
),
|
||
'q3', jsonb_build_object(
|
||
'base_plan', s.col_q3_base_plan,
|
||
'corrected_plan', s.col_q3_corrected_plan,
|
||
'carryover', s.col_q3_carryover,
|
||
'adj_by_items', s.col_q3_adj_by_items,
|
||
'adj_increase', s.col_q3_adj_increase,
|
||
'total_corr', s.col_q3_total_corr,
|
||
'm1', s.col_q3_m1,
|
||
'm2', s.col_q3_m2,
|
||
'm3', s.col_q3_m3,
|
||
'quarter_actual', s.col_q3_quarter_actual,
|
||
'economy', s.col_q3_economy
|
||
),
|
||
'q4', jsonb_build_object(
|
||
'base_plan', s.col_q4_base_plan,
|
||
'corrected_plan', s.col_q4_corrected_plan,
|
||
'carryover', s.col_q4_carryover,
|
||
'adj_by_items', s.col_q4_adj_by_items,
|
||
'adj_increase', s.col_q4_adj_increase,
|
||
'total_corr', s.col_q4_total_corr,
|
||
'm1', s.col_q4_m1,
|
||
'm2', s.col_q4_m2,
|
||
'm3', s.col_q4_m3,
|
||
'spod', s.col_q4_spod,
|
||
'quarter_actual', s.col_q4_quarter_actual,
|
||
'economy', s.col_q4_economy
|
||
),
|
||
'totals', jsonb_build_object(
|
||
'base_plan', s.col_year_base_plan, -- Год Базового плана (Σ q1–q4)
|
||
'total_corr', s.col_year_total_corr,
|
||
'total_actual', s.col_year_total_actual,
|
||
'economy', s.col_year_economy
|
||
)
|
||
) AS data
|
||
FROM v3.v_form3_report_sections(p_report_id, p_sections) s
|
||
ORDER BY s._sort_path;
|
||
$function$
|
||
LANGUAGE sql STABLE;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- 09_backfill_existing.sql — донакат на СУЩЕСТВУЮЩИЕ отчёты FORM_3
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Нужен ТОЛЬКО если в БД уже есть отчёты rf_project_report, созданные до этого
|
||
-- апдейта. На чистой БД / для новых проектов не требуется (add_project и
|
||
-- add_project_year уже дают правильные снапшоты и сетку).
|
||
--
|
||
-- 1) copy_template_to_report использует ON CONFLICT DO NOTHING → у старых
|
||
-- отчётов в form3_phase остаются СТАРЫЕ column_keys (без base_plan/
|
||
-- corrected_plan). Ресинкаем column_keys из обновлённого phase_template.
|
||
-- 2) (ОПЦИОНАЛЬНО) досеиваем фиксированную сетку статей для старых отчётов.
|
||
-- ⚠ Меняет набор строк старых отчётов (добавит все листовые статьи FORM_3).
|
||
-- Раскомментируйте, только если хотите привести старые отчёты к фикс-сетке.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
-- 1. Ресинк column_keys существующих снапшотов с шаблоном (только добавляет
|
||
-- новые ключи план-блоков; окна opens_at/closes_at не трогаем).
|
||
UPDATE v3.form3_phase fp
|
||
SET column_keys = pt.column_keys
|
||
FROM v3.rf_project_report r,
|
||
v3.phase_template pt
|
||
WHERE fp.rf_project_report_id = r.id
|
||
AND pt.form_type = 'FORM_3'
|
||
AND pt.sheet = r.report_type
|
||
AND pt.phase_code = fp.phase_code
|
||
AND pt.role = fp.role
|
||
AND fp.column_keys IS DISTINCT FROM pt.column_keys;
|
||
|
||
-- Также подхватить фазы, которых у старого отчёта вовсе не было (идемпотентно).
|
||
SELECT v3.copy_template_to_report(id) FROM v3.rf_project_report;
|
||
|
||
-- 2. ОПЦИОНАЛЬНО: фиксированная сетка статей для старых отчётов.
|
||
-- Раскомментируйте при необходимости:
|
||
-- SELECT v3.seed_form3_report_lines(id) FROM v3.rf_project_report;
|
||
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.v_form3_project_jsonb — многолетний широкий лист ЛП/ТР одного проекта
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Новый шаблон FORM_3 (v2) показывает годы проекта рядом (год1|год2|…) —
|
||
-- «кварталы повторяются для каждого года проекта». Горизонт переменный: сколько
|
||
-- у проекта заведено годовых rf_project_report того же трека, столько year-групп.
|
||
--
|
||
-- Модель: каждый (год × трек) — отдельный rf_project_report со своим набором
|
||
-- строк (фиксированная сетка статей, одинаковая во всех годах). Здесь мы сшиваем
|
||
-- эти годовые отчёты по СТАТЬЕ (expense_item_id — стабильный ключ между годами;
|
||
-- line_id у строки в каждом году свой и лежит внутри года — для адресной правки).
|
||
--
|
||
-- Возврат — общий формат UI (row_type, depth, sort_order, data), где data:
|
||
-- { "header": {...}, -- общая шапка строки (одна на все годы)
|
||
-- "years": { "2025": <год-объект>, ... },-- год → data из v_form3_report_jsonb
|
||
-- -- (внутри line_id года + q1..q4 + totals)
|
||
-- "project_total": <NUMERIC> } -- «Стоимость проекта за N лет» =
|
||
-- -- Σ годовых фактических итогов (totals.total_actual)
|
||
--
|
||
-- p_report_type — 'LIMIT' (ЛП) либо 'CURRENT_EXPENSES' (ТР).
|
||
-- p_sections — как в v_form3_report_sections ('q1'..'q4','year'), прокидывается
|
||
-- в каждый год.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
DROP FUNCTION IF EXISTS v3.v_form3_project_jsonb(INT, VARCHAR, TEXT[]);
|
||
|
||
CREATE FUNCTION v3.v_form3_project_jsonb(
|
||
p_project_id INT,
|
||
p_report_type VARCHAR,
|
||
p_sections TEXT[] DEFAULT NULL
|
||
)
|
||
RETURNS TABLE (
|
||
row_type VARCHAR,
|
||
depth INT,
|
||
sort_order BIGINT,
|
||
data JSONB
|
||
)
|
||
AS $function$
|
||
WITH reports AS (
|
||
SELECT r.id AS report_id, r.year
|
||
FROM v3.rf_project_report r
|
||
WHERE r.project_id = p_project_id
|
||
AND r.report_type = p_report_type
|
||
),
|
||
-- Разворачиваем каждый год в строки его листа
|
||
per_year AS (
|
||
SELECT
|
||
v.row_type,
|
||
v.depth,
|
||
(v.data->'header'->>'expense_item_id')::INT AS eiid,
|
||
v.data->'header' AS header,
|
||
v.sort_order AS y_sort,
|
||
rp.year AS year,
|
||
v.data AS ydata
|
||
FROM reports rp
|
||
CROSS JOIN LATERAL v3.v_form3_report_jsonb(rp.report_id, p_sections) v
|
||
),
|
||
-- Сшиваем по идентичности строки (row_type + статья): сетка одинакова во всех годах
|
||
agg AS (
|
||
SELECT
|
||
py.row_type,
|
||
py.depth,
|
||
py.eiid,
|
||
MIN(py.y_sort) AS sort_order,
|
||
(array_agg(py.header ORDER BY py.year))[1] AS header,
|
||
jsonb_object_agg(py.year::text, py.ydata ORDER BY py.year) AS years,
|
||
SUM((py.ydata->'totals'->>'total_actual')::numeric) AS project_total
|
||
FROM per_year py
|
||
GROUP BY py.row_type, py.depth, py.eiid
|
||
)
|
||
SELECT
|
||
a.row_type,
|
||
a.depth,
|
||
ROW_NUMBER() OVER (ORDER BY a.sort_order) AS sort_order,
|
||
jsonb_build_object(
|
||
'header', a.header,
|
||
'years', a.years,
|
||
'project_total', a.project_total
|
||
) AS data
|
||
FROM agg a
|
||
ORDER BY a.sort_order;
|
||
$function$
|
||
LANGUAGE sql STABLE;
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- v3.phase_template — seed (фазы редактирования: роль × лист × окно × колонки)
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Шаблон редактируется админом. Изменения здесь НЕ влияют на уже созданные
|
||
-- формы — у них свой snapshot в v3.form_phase.
|
||
--
|
||
-- Этот seed — стартовая разметка, в которой жёстко прошита только семантика
|
||
-- sequestration / seq_ssp / seq_dfip (где actor однозначно следует из v3-кода).
|
||
-- Остальные колонки (plan, q1.adj_*, contract_*, allocation, reserve, ckk,
|
||
-- collegial, header.*, ...) НЕ покрыты — добавлять по мере согласования
|
||
-- бизнес-правила «кто правит в какую фазу».
|
||
--
|
||
-- Для smoke-теста все фазы открыты широко (весь 2026). В проде окна должны
|
||
-- соответствовать реальному регламенту бюджетного цикла.
|
||
--
|
||
-- Возвратные фазы (EXECUTOR_RF→DFIP→EXECUTOR_RF) — несколько строк с одной
|
||
-- ролью, разные phase_code и окна, без особых случаев.
|
||
--
|
||
-- ⚠ role ссылается на v3.role(code): ADMIN / DFIP / EXECUTOR_RF. ADMIN в
|
||
-- шаблоне быть не должен (в can_edit делается bypass) — CHECK-констрейнт
|
||
-- chk_v3_phase_template_no_admin отрежет.
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TRUNCATE TABLE v3.phase_template;
|
||
|
||
-- ─── FORM_1: actor sequestration = DFIP (alias на одно поле в v3.sequestration)
|
||
INSERT INTO v3.phase_template
|
||
(form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
||
VALUES
|
||
('FORM_1', 'AHR', 'DFIP_SEQ_2026', 'DFIP',
|
||
ARRAY['sequestration.q1','sequestration.q2','sequestration.q3','sequestration.q4',
|
||
'sequestration.justification'],
|
||
'2026-01-01 00:00:00+00', '2026-12-31 23:59:59+00'),
|
||
('FORM_1', 'CAP', 'DFIP_SEQ_2026', 'DFIP',
|
||
ARRAY['sequestration.q1','sequestration.q2','sequestration.q3','sequestration.q4',
|
||
'sequestration.justification'],
|
||
'2026-01-01 00:00:00+00', '2026-12-31 23:59:59+00'),
|
||
('FORM_1', 'OPER', 'DFIP_SEQ_2026', 'DFIP',
|
||
ARRAY['sequestration.q1','sequestration.q2','sequestration.q3','sequestration.q4',
|
||
'sequestration.justification'],
|
||
'2026-01-01 00:00:00+00', '2026-12-31 23:59:59+00');
|
||
|
||
-- ─── FORM_2 / FORM_4: разнесены по actor через scope (seq_ssp / seq_dfip)
|
||
INSERT INTO v3.phase_template
|
||
(form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
||
SELECT ft, sh, ph, rl, ck,
|
||
'2026-01-01 00:00:00+00'::timestamptz,
|
||
'2026-12-31 23:59:59+00'::timestamptz
|
||
FROM (VALUES
|
||
('FORM_2','AHR'), ('FORM_2','CAP'), ('FORM_2','OPER'),
|
||
('FORM_4','AHR'), ('FORM_4','CAP'), ('FORM_4','OPER')
|
||
) AS f(ft, sh)
|
||
CROSS JOIN (VALUES
|
||
('SSP_SEQ_2026', 'EXECUTOR_RF', ARRAY['seq_ssp.q1','seq_ssp.q2','seq_ssp.q3','seq_ssp.q4',
|
||
'seq_ssp.justification']),
|
||
('DFIP_SEQ_2026', 'DFIP', ARRAY['seq_dfip.q1','seq_dfip.q2','seq_dfip.q3','seq_dfip.q4',
|
||
'seq_dfip.justification'])
|
||
) AS p(ph, rl, ck);
|
||
|
||
-- ─── FORM_3: sheet = report_type ('LIMIT' / 'CURRENT_EXPENSES') ────────────
|
||
-- Ветка rf_project_report. column_keys — scope.field формата qN.<field>
|
||
-- (см. upd_form3_cell.sql). Год несёт report_id (каждый год проекта — свой
|
||
-- rf_project_report), поэтому ключи БЕЗ префикса года. Наборы полей:
|
||
-- Факт: adj_by_items / adj_increase / m1 / m2 / m3 по кварталу + q4.spod
|
||
-- Базовый план: q1..q4.base_plan
|
||
-- Скоррект план: q2..q4.corrected_plan (без q1 — шаблон с II квартала)
|
||
-- EXECUTOR_RF — основной заполнитель отчётов РФ. Стартовая разметка: одна
|
||
-- широкая фаза заполнения на весь 2026 для обоих треков. Реальные окна/роли
|
||
-- (в т.ч. проверка DFIP) — уточнять по регламенту.
|
||
INSERT INTO v3.phase_template
|
||
(form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
||
SELECT 'FORM_3', sh, 'RF_FILL_2026', 'EXECUTOR_RF',
|
||
ARRAY[
|
||
-- Факт
|
||
'q1.adj_by_items','q1.adj_increase','q1.m1','q1.m2','q1.m3',
|
||
'q2.adj_by_items','q2.adj_increase','q2.m1','q2.m2','q2.m3',
|
||
'q3.adj_by_items','q3.adj_increase','q3.m1','q3.m2','q3.m3',
|
||
'q4.adj_by_items','q4.adj_increase','q4.m1','q4.m2','q4.m3','q4.spod',
|
||
-- Базовый план (q1–q4)
|
||
'q1.base_plan','q2.base_plan','q3.base_plan','q4.base_plan',
|
||
-- Скоррект план (q2–q4)
|
||
'q2.corrected_plan','q3.corrected_plan','q4.corrected_plan'
|
||
],
|
||
'2026-01-01 00:00:00+00'::timestamptz,
|
||
'2026-12-31 23:59:59+00'::timestamptz
|
||
FROM (VALUES ('LIMIT'), ('CURRENT_EXPENSES')) AS s(sh);
|
||
|
||
-- Образец возвратной DFIP-фазы (ГО правит корректировки после РФ):
|
||
-- INSERT INTO v3.phase_template
|
||
-- (form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
||
-- VALUES ('FORM_3', 'CURRENT_EXPENSES', 'DFIP_REVIEW_2026', 'DFIP',
|
||
-- ARRAY['q1.adj_by_items','q1.adj_increase','q2.adj_by_items','q2.adj_increase'],
|
||
-- '2026-04-01 00:00:00+00', '2026-04-30 23:59:59+00');
|
||
|
||
-- ─── TODO: phase для plan, q1.adj_*, contract, allocation, reserve, etc. ───
|
||
-- Образец на будущее — план EXECUTOR_RF в первое окно цикла:
|
||
--
|
||
-- INSERT INTO v3.phase_template
|
||
-- (form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
||
-- VALUES (
|
||
-- 'FORM_1', 'AHR', 'SSP_PLAN_INITIAL_2026', 'EXECUTOR_RF',
|
||
-- ARRAY['plan.q1','plan.q2','plan.q3','plan.q4','plan.comment'],
|
||
-- '2026-02-01 00:00:00+00', '2026-02-28 23:59:59+00'
|
||
-- );
|
||
--
|
||
-- Полный список scope.field — см. v3/upd_form_cell.sql (комментарий вверху
|
||
-- файла + блоки в _apply_form_cell).
|