Слил дублирубщие номера миграций
This commit is contained in:
parent
12dc8414d1
commit
155ab65b58
@ -1,15 +0,0 @@
|
|||||||
from sql_migration import run_sql_file_migration
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0050"
|
|
||||||
down_revision = "0049"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
run_sql_file_migration("0050_project_technical_number_lock.sql")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
from sql_migration import run_sql_file_migration
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0051"
|
|
||||||
down_revision = "0050"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
run_sql_file_migration("0051_upd_form_cells_full_rows.sql")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
from sql_migration import run_sql_file_migration
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0052"
|
|
||||||
down_revision = "0051"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
run_sql_file_migration("0052_upd_form_cells_subtree.sql")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
from sql_migration import run_sql_file_migration
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0053"
|
|
||||||
down_revision = "0052"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
run_sql_file_migration("0053_form4_sheet_sum.sql")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1626,3 +1626,101 @@ UPDATE v3.form_phase
|
|||||||
WHERE sheet IN ('AHR_RENT', 'AHR_UTILITY', 'AHR_SECURITY')
|
WHERE sheet IN ('AHR_RENT', 'AHR_UTILITY', 'AHR_SECURITY')
|
||||||
AND 'fact_q4.dec' = ANY(column_keys)
|
AND 'fact_q4.dec' = ANY(column_keys)
|
||||||
AND NOT ('fact_q4.spod' = ANY(column_keys));
|
AND NOT ('fact_q4.spod' = ANY(column_keys));
|
||||||
|
|
||||||
|
|
||||||
|
-- AURORA-1831: гонка technical_number при параллельном создании проектов.
|
||||||
|
-- v3.add_project: копия версии из 0018 + advisory lock перед вычислением счётчика
|
||||||
|
-- и понятная ошибка при переполнении счётчика (больше 99 проектов на оргединицу).
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
|
||||||
|
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_lim INT;
|
||||||
|
v_cur INT;
|
||||||
|
v_seq INT;
|
||||||
|
v_technical_number VARCHAR;
|
||||||
|
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 ou WHERE id = p_org_unit_id AND ou.is_active) 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;
|
||||||
|
|
||||||
|
-- AURORA-1831: сериализуем выдачу номера внутри одной оргединицы. Без блокировки
|
||||||
|
-- две параллельные транзакции читают один и тот же MAX и падают на уникальном индексе.
|
||||||
|
-- Блокировка снимается на COMMIT/ROLLBACK. Решение опирается на READ COMMITTED
|
||||||
|
-- (приложение не меняет изоляцию): после получения блокировки SELECT MAX берёт новый
|
||||||
|
-- снимок и видит строку, закоммиченную предыдущим владельцем. При REPEATABLE READ
|
||||||
|
-- и выше этого недостаточно.
|
||||||
|
PERFORM pg_advisory_xact_lock(hashtext('v3.project.technical_number'), p_org_unit_id);
|
||||||
|
|
||||||
|
-- Порядковый счётчик проекта для данного РФ/ВСП (org_unit)
|
||||||
|
SELECT COALESCE(MAX(right(technical_number, 2)::int), 0) + 1
|
||||||
|
INTO v_seq
|
||||||
|
FROM v3.project
|
||||||
|
WHERE org_unit_id = p_org_unit_id
|
||||||
|
AND technical_number IS NOT NULL;
|
||||||
|
|
||||||
|
IF v_seq > 99 THEN
|
||||||
|
RAISE EXCEPTION 'Исчерпан счётчик технических номеров проектов для org_unit id=% (максимум 99)', p_org_unit_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_technical_number := to_char(now(), 'YYMM') || '_'
|
||||||
|
|| lpad(p_org_unit_id::text, 4, '0')
|
||||||
|
|| lpad(v_seq::text, 2, '0');
|
||||||
|
|
||||||
|
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, technical_number
|
||||||
|
) 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, v_technical_number
|
||||||
|
) RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
||||||
|
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',
|
||||||
|
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,
|
||||||
|
'technical_number', v_technical_number,
|
||||||
|
'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$;
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
-- AURORA-1831: гонка technical_number при параллельном создании проектов.
|
|
||||||
-- v3.add_project: копия версии из 0018 + advisory lock перед вычислением счётчика
|
|
||||||
-- и понятная ошибка при переполнении счётчика (больше 99 проектов на оргединицу).
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
|
|
||||||
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_pid INT;
|
|
||||||
v_lim INT;
|
|
||||||
v_cur INT;
|
|
||||||
v_seq INT;
|
|
||||||
v_technical_number VARCHAR;
|
|
||||||
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 ou WHERE id = p_org_unit_id AND ou.is_active) 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;
|
|
||||||
|
|
||||||
-- AURORA-1831: сериализуем выдачу номера внутри одной оргединицы. Без блокировки
|
|
||||||
-- две параллельные транзакции читают один и тот же MAX и падают на уникальном индексе.
|
|
||||||
-- Блокировка снимается на COMMIT/ROLLBACK. Решение опирается на READ COMMITTED
|
|
||||||
-- (приложение не меняет изоляцию): после получения блокировки SELECT MAX берёт новый
|
|
||||||
-- снимок и видит строку, закоммиченную предыдущим владельцем. При REPEATABLE READ
|
|
||||||
-- и выше этого недостаточно.
|
|
||||||
PERFORM pg_advisory_xact_lock(hashtext('v3.project.technical_number'), p_org_unit_id);
|
|
||||||
|
|
||||||
-- Порядковый счётчик проекта для данного РФ/ВСП (org_unit)
|
|
||||||
SELECT COALESCE(MAX(right(technical_number, 2)::int), 0) + 1
|
|
||||||
INTO v_seq
|
|
||||||
FROM v3.project
|
|
||||||
WHERE org_unit_id = p_org_unit_id
|
|
||||||
AND technical_number IS NOT NULL;
|
|
||||||
|
|
||||||
IF v_seq > 99 THEN
|
|
||||||
RAISE EXCEPTION 'Исчерпан счётчик технических номеров проектов для org_unit id=% (максимум 99)', p_org_unit_id;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
v_technical_number := to_char(now(), 'YYMM') || '_'
|
|
||||||
|| lpad(p_org_unit_id::text, 4, '0')
|
|
||||||
|| lpad(v_seq::text, 2, '0');
|
|
||||||
|
|
||||||
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, technical_number
|
|
||||||
) 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, v_technical_number
|
|
||||||
) RETURNING id INTO v_pid;
|
|
||||||
|
|
||||||
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
|
||||||
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',
|
|
||||||
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,
|
|
||||||
'technical_number', v_technical_number,
|
|
||||||
'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$;
|
|
||||||
@ -226,3 +226,200 @@ $function$;
|
|||||||
-- нигде не вызывают (актуальный путь — ...detail_sync_line/...detail_sync_values).
|
-- нигде не вызывают (актуальный путь — ...detail_sync_line/...detail_sync_values).
|
||||||
DROP FUNCTION IF EXISTS v3._upsert_form2_ahr_sync_values(INT, INT, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC);
|
DROP FUNCTION IF EXISTS v3._upsert_form2_ahr_sync_values(INT, INT, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC);
|
||||||
DROP FUNCTION IF EXISTS v3._ensure_form2_ahr_sync_line(INT, INT);
|
DROP FUNCTION IF EXISTS v3._ensure_form2_ahr_sync_line(INT, INT);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- AURORA-1844: v3.upd_form_cells возвращает затронутые строки целиком.
|
||||||
|
-- Убран снимок «до» и _jsonb_diff: после применения правок выполняется один
|
||||||
|
-- вызов v_form_view (внутри v3._form_affected_rows), ответ — полные строки
|
||||||
|
-- изменённых INPUT и всех их предков-агрегатов (включая неизменившиеся).
|
||||||
|
-- Обёртка с p_user_id (0031) и upd_form_cell не меняются.
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3._form_affected_rows(p_form_id integer, p_sheet character varying, p_ids integer[], p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
|
||||||
|
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_form_type VARCHAR;
|
||||||
|
v_strip TEXT[] := ARRAY[]::TEXT[];
|
||||||
|
v_lc_path INT[];
|
||||||
|
v_eid_set INT[];
|
||||||
|
v_ei_path INT[];
|
||||||
|
BEGIN
|
||||||
|
SELECT bf.form_type_code INTO v_form_type
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
WHERE bf.id = p_form_id;
|
||||||
|
|
||||||
|
-- Ключи верхнего уровня исключённых разделов (при p_sections они приходят как null
|
||||||
|
-- и затёрли бы данные клиента). Вложенные null не трогаем: это очистка ячейки.
|
||||||
|
IF p_sections IS NOT NULL THEN
|
||||||
|
SELECT COALESCE(array_agg(k), ARRAY[]::TEXT[])
|
||||||
|
INTO v_strip
|
||||||
|
FROM unnest(ARRAY['plan','seq_dfip','seq_ssp','approved','contract','booking',
|
||||||
|
'contract_summary','allocation','collegial','ckk',
|
||||||
|
'sequestration','reserve','contract_detail',
|
||||||
|
'q1','q2','q3','q4','totals',
|
||||||
|
'fact_q1','fact_q2','fact_q3','fact_q4']) AS k
|
||||||
|
WHERE NOT (k = ANY(p_sections));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||||
|
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(p_ids)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
|
||||||
|
ELSIF p_sheet = 'AHR_LIMIT' THEN
|
||||||
|
v_lc_path := v3._limit_template_path_set(p_ids);
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||||
|
WHERE (v.data->>'id')::INT = ANY(v_lc_path)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
|
||||||
|
ELSIF p_sheet = 'OTCH9F' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM v3.fixed_asset_report far
|
||||||
|
WHERE far.id = ANY(p_ids)
|
||||||
|
AND far.expense_item_id = (v.data->>'expense_item_id')::INT
|
||||||
|
AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'))
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
SELECT array_agg(DISTINCT bl.expense_item_id)
|
||||||
|
INTO v_eid_set
|
||||||
|
FROM v3.budget_line bl
|
||||||
|
WHERE bl.id = ANY(p_ids);
|
||||||
|
v_ei_path := v3._expense_item_path_set(v_eid_set);
|
||||||
|
|
||||||
|
IF v_form_type = 'FORM_4' THEN
|
||||||
|
-- ROOT раздела, GROUP по program_id и ITEM по project_id изменённых строк
|
||||||
|
-- (у GROUP/ITEM FORM_4 expense_item_id = id раздела, поэтому фильтр
|
||||||
|
-- по expense_item_id захватил бы все программы и проекты раздела).
|
||||||
|
RETURN QUERY
|
||||||
|
WITH vv AS MATERIALIZED (
|
||||||
|
SELECT f.row_type, f.depth, f.sort_order, f.data
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) f
|
||||||
|
WHERE f.row_type <> 'META'
|
||||||
|
),
|
||||||
|
chg AS (
|
||||||
|
SELECT DISTINCT (x.data->'header'->>'program_id')::INT AS program_id,
|
||||||
|
(x.data->'header'->>'project_id')::INT AS project_id
|
||||||
|
FROM vv x
|
||||||
|
WHERE x.row_type = 'INPUT' AND (x.data->>'line_id')::INT = ANY(p_ids)
|
||||||
|
)
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||||
|
FROM vv v
|
||||||
|
WHERE (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(p_ids))
|
||||||
|
OR (v.row_type = 'ROOT'
|
||||||
|
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
|
||||||
|
OR (v.row_type = 'GROUP'
|
||||||
|
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
|
||||||
|
AND (v.data->'header'->>'program_id')::INT IN (SELECT c.program_id FROM chg c))
|
||||||
|
OR (v.row_type = 'ITEM'
|
||||||
|
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
|
||||||
|
AND (v.data->'header'->>'project_id')::INT IN (SELECT c.project_id FROM chg c))
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
ELSE
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||||
|
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_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(p_ids))
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
|
||||||
|
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_change JSONB;
|
||||||
|
v_line_id INT;
|
||||||
|
v_column TEXT;
|
||||||
|
v_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.budget_form WHERE id = p_form_id) THEN
|
||||||
|
RAISE EXCEPTION 'budget_form id=% не существует', p_form_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';
|
||||||
|
|
||||||
|
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 p_sheet = 'AHR_LIMIT' THEN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt
|
||||||
|
WHERE lt.id = v_line_id AND lt.row_type = 'LEAF') THEN
|
||||||
|
RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF', v_line_id;
|
||||||
|
END IF;
|
||||||
|
ELSIF p_sheet = 'OTCH9F' THEN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.fixed_asset_report
|
||||||
|
WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
|
||||||
|
RAISE EXCEPTION 'fixed_asset_report.id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||||
|
END IF;
|
||||||
|
ELSIF p_sheet = 'AHR_RENT' THEN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.rent_detail rd
|
||||||
|
JOIN v3.budget_line bl ON bl.id = rd.line_id
|
||||||
|
WHERE rd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
||||||
|
RAISE EXCEPTION 'rent_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||||
|
END IF;
|
||||||
|
ELSIF p_sheet = 'AHR_UTILITY' THEN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.utility_detail ud
|
||||||
|
JOIN v3.budget_line bl ON bl.id = ud.line_id
|
||||||
|
WHERE ud.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
||||||
|
RAISE EXCEPTION 'utility_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||||
|
END IF;
|
||||||
|
ELSIF p_sheet = 'AHR_SECURITY' THEN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.security_detail sd
|
||||||
|
JOIN v3.budget_line bl ON bl.id = sd.line_id
|
||||||
|
WHERE sd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
||||||
|
RAISE EXCEPTION 'security_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.budget_line
|
||||||
|
WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
|
||||||
|
RAISE EXCEPTION 'budget_line id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT (v_line_id = ANY(v_changed_lines)) THEN
|
||||||
|
v_changed_lines := v_changed_lines || v_line_id;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
|
||||||
|
LOOP
|
||||||
|
PERFORM v3._apply_form_cell(
|
||||||
|
(v_change->>'line_id')::INT,
|
||||||
|
v_change->>'column',
|
||||||
|
v_change->'value',
|
||||||
|
p_sheet,
|
||||||
|
p_form_id);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT r.row_type, r.depth, r.sort_order, r.data
|
||||||
|
FROM v3._form_affected_rows(p_form_id, p_sheet, v_changed_lines, p_direction, p_sections) r
|
||||||
|
ORDER BY r.sort_order;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
|||||||
@ -1,194 +0,0 @@
|
|||||||
-- AURORA-1844: v3.upd_form_cells возвращает затронутые строки целиком.
|
|
||||||
-- Убран снимок «до» и _jsonb_diff: после применения правок выполняется один
|
|
||||||
-- вызов v_form_view (внутри v3._form_affected_rows), ответ — полные строки
|
|
||||||
-- изменённых INPUT и всех их предков-агрегатов (включая неизменившиеся).
|
|
||||||
-- Обёртка с p_user_id (0031) и upd_form_cell не меняются.
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3._form_affected_rows(p_form_id integer, p_sheet character varying, p_ids integer[], p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
|
|
||||||
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_form_type VARCHAR;
|
|
||||||
v_strip TEXT[] := ARRAY[]::TEXT[];
|
|
||||||
v_lc_path INT[];
|
|
||||||
v_eid_set INT[];
|
|
||||||
v_ei_path INT[];
|
|
||||||
BEGIN
|
|
||||||
SELECT bf.form_type_code INTO v_form_type
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
WHERE bf.id = p_form_id;
|
|
||||||
|
|
||||||
-- Ключи верхнего уровня исключённых разделов (при p_sections они приходят как null
|
|
||||||
-- и затёрли бы данные клиента). Вложенные null не трогаем: это очистка ячейки.
|
|
||||||
IF p_sections IS NOT NULL THEN
|
|
||||||
SELECT COALESCE(array_agg(k), ARRAY[]::TEXT[])
|
|
||||||
INTO v_strip
|
|
||||||
FROM unnest(ARRAY['plan','seq_dfip','seq_ssp','approved','contract','booking',
|
|
||||||
'contract_summary','allocation','collegial','ckk',
|
|
||||||
'sequestration','reserve','contract_detail',
|
|
||||||
'q1','q2','q3','q4','totals',
|
|
||||||
'fact_q1','fact_q2','fact_q3','fact_q4']) AS k
|
|
||||||
WHERE NOT (k = ANY(p_sections));
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
|
||||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
|
||||||
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(p_ids)
|
|
||||||
ORDER BY v.sort_order;
|
|
||||||
|
|
||||||
ELSIF p_sheet = 'AHR_LIMIT' THEN
|
|
||||||
v_lc_path := v3._limit_template_path_set(p_ids);
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
|
||||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
|
||||||
WHERE (v.data->>'id')::INT = ANY(v_lc_path)
|
|
||||||
ORDER BY v.sort_order;
|
|
||||||
|
|
||||||
ELSIF p_sheet = 'OTCH9F' THEN
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
|
||||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
|
||||||
WHERE EXISTS (
|
|
||||||
SELECT 1 FROM v3.fixed_asset_report far
|
|
||||||
WHERE far.id = ANY(p_ids)
|
|
||||||
AND far.expense_item_id = (v.data->>'expense_item_id')::INT
|
|
||||||
AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'))
|
|
||||||
ORDER BY v.sort_order;
|
|
||||||
|
|
||||||
ELSE
|
|
||||||
SELECT array_agg(DISTINCT bl.expense_item_id)
|
|
||||||
INTO v_eid_set
|
|
||||||
FROM v3.budget_line bl
|
|
||||||
WHERE bl.id = ANY(p_ids);
|
|
||||||
v_ei_path := v3._expense_item_path_set(v_eid_set);
|
|
||||||
|
|
||||||
IF v_form_type = 'FORM_4' THEN
|
|
||||||
-- ROOT раздела, GROUP по program_id и ITEM по project_id изменённых строк
|
|
||||||
-- (у GROUP/ITEM FORM_4 expense_item_id = id раздела, поэтому фильтр
|
|
||||||
-- по expense_item_id захватил бы все программы и проекты раздела).
|
|
||||||
RETURN QUERY
|
|
||||||
WITH vv AS MATERIALIZED (
|
|
||||||
SELECT f.row_type, f.depth, f.sort_order, f.data
|
|
||||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) f
|
|
||||||
WHERE f.row_type <> 'META'
|
|
||||||
),
|
|
||||||
chg AS (
|
|
||||||
SELECT DISTINCT (x.data->'header'->>'program_id')::INT AS program_id,
|
|
||||||
(x.data->'header'->>'project_id')::INT AS project_id
|
|
||||||
FROM vv x
|
|
||||||
WHERE x.row_type = 'INPUT' AND (x.data->>'line_id')::INT = ANY(p_ids)
|
|
||||||
)
|
|
||||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
|
||||||
FROM vv v
|
|
||||||
WHERE (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(p_ids))
|
|
||||||
OR (v.row_type = 'ROOT'
|
|
||||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
|
|
||||||
OR (v.row_type = 'GROUP'
|
|
||||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
|
|
||||||
AND (v.data->'header'->>'program_id')::INT IN (SELECT c.program_id FROM chg c))
|
|
||||||
OR (v.row_type = 'ITEM'
|
|
||||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
|
|
||||||
AND (v.data->'header'->>'project_id')::INT IN (SELECT c.project_id FROM chg c))
|
|
||||||
ORDER BY v.sort_order;
|
|
||||||
ELSE
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
|
||||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
|
||||||
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_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(p_ids))
|
|
||||||
ORDER BY v.sort_order;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
|
|
||||||
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_change JSONB;
|
|
||||||
v_line_id INT;
|
|
||||||
v_column TEXT;
|
|
||||||
v_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.budget_form WHERE id = p_form_id) THEN
|
|
||||||
RAISE EXCEPTION 'budget_form id=% не существует', p_form_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';
|
|
||||||
|
|
||||||
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 p_sheet = 'AHR_LIMIT' THEN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt
|
|
||||||
WHERE lt.id = v_line_id AND lt.row_type = 'LEAF') THEN
|
|
||||||
RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF', v_line_id;
|
|
||||||
END IF;
|
|
||||||
ELSIF p_sheet = 'OTCH9F' THEN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM v3.fixed_asset_report
|
|
||||||
WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
|
|
||||||
RAISE EXCEPTION 'fixed_asset_report.id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
|
||||||
END IF;
|
|
||||||
ELSIF p_sheet = 'AHR_RENT' THEN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM v3.rent_detail rd
|
|
||||||
JOIN v3.budget_line bl ON bl.id = rd.line_id
|
|
||||||
WHERE rd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
|
||||||
RAISE EXCEPTION 'rent_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
|
||||||
END IF;
|
|
||||||
ELSIF p_sheet = 'AHR_UTILITY' THEN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM v3.utility_detail ud
|
|
||||||
JOIN v3.budget_line bl ON bl.id = ud.line_id
|
|
||||||
WHERE ud.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
|
||||||
RAISE EXCEPTION 'utility_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
|
||||||
END IF;
|
|
||||||
ELSIF p_sheet = 'AHR_SECURITY' THEN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM v3.security_detail sd
|
|
||||||
JOIN v3.budget_line bl ON bl.id = sd.line_id
|
|
||||||
WHERE sd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
|
||||||
RAISE EXCEPTION 'security_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
|
||||||
END IF;
|
|
||||||
ELSE
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM v3.budget_line
|
|
||||||
WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
|
|
||||||
RAISE EXCEPTION 'budget_line id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT (v_line_id = ANY(v_changed_lines)) THEN
|
|
||||||
v_changed_lines := v_changed_lines || v_line_id;
|
|
||||||
END IF;
|
|
||||||
END LOOP;
|
|
||||||
|
|
||||||
FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
|
|
||||||
LOOP
|
|
||||||
PERFORM v3._apply_form_cell(
|
|
||||||
(v_change->>'line_id')::INT,
|
|
||||||
v_change->>'column',
|
|
||||||
v_change->'value',
|
|
||||||
p_sheet,
|
|
||||||
p_form_id);
|
|
||||||
END LOOP;
|
|
||||||
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT r.row_type, r.depth, r.sort_order, r.data
|
|
||||||
FROM v3._form_affected_rows(p_form_id, p_sheet, v_changed_lines, p_direction, p_sections) r
|
|
||||||
ORDER BY r.sort_order;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -823,3 +823,609 @@ AS $function$
|
|||||||
ORDER BY s._sort_path;
|
ORDER BY s._sort_path;
|
||||||
$function$
|
$function$
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- AURORA-1846: FORM_4, замена рекурсивного агрегата v3.jsonb_sum в
|
||||||
|
-- v3.v_form4_sheet_jsonb на разворот листьев jsonb + GROUPING SETS.
|
||||||
|
-- Тело — копия sql/0012_del_project.sql:1091-1617, изменён только блок
|
||||||
|
-- «4. Агрегаты». v3.jsonb_sum / v3.jsonb_numeric_sum не удаляются (эталон в тестах).
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
|
||||||
|
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
SET jit TO 'off' -- оценка плана выше порога JIT, компиляция дороже выполнения (AURORA-1846)
|
||||||
|
AS $function$
|
||||||
|
-- ═══ 1. Базовые INPUT-строки с расширенной иерархией ═══════════════════════
|
||||||
|
WITH inp_raw AS (
|
||||||
|
SELECT j.*
|
||||||
|
FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j
|
||||||
|
WHERE j.row_type = 'INPUT'
|
||||||
|
),
|
||||||
|
inp AS (
|
||||||
|
SELECT
|
||||||
|
i.*,
|
||||||
|
bl.project_id AS line_project_id,
|
||||||
|
|
||||||
|
-- --- разрешение проекта ---
|
||||||
|
-- A. bl.project_id → project (level='project')
|
||||||
|
-- B. bl.project_id → program (level='program') — тогда проекта нет
|
||||||
|
CASE WHEN prj.id IS NOT NULL THEN prj.id END AS prj_id,
|
||||||
|
CASE WHEN prj.id IS NOT NULL THEN prj.name END AS prj_name,
|
||||||
|
|
||||||
|
-- --- разрешение программы ---
|
||||||
|
-- A. через проект: prog_via_prj
|
||||||
|
-- B. напрямую: prog_direct
|
||||||
|
COALESCE(prog_via_prj.id, prog_direct.id) AS prog_id,
|
||||||
|
COALESCE(prog_via_prj.name, prog_direct.name) AS prog_name,
|
||||||
|
|
||||||
|
-- --- section (depth=0) ---
|
||||||
|
sec.id AS sec_id,
|
||||||
|
sec.section_code AS sec_code,
|
||||||
|
sec.name AS sec_name
|
||||||
|
|
||||||
|
FROM inp_raw i
|
||||||
|
JOIN v3.budget_line bl ON bl.id = i.line_id
|
||||||
|
|
||||||
|
-- проект (level='project')
|
||||||
|
LEFT JOIN v3.form4_project prj
|
||||||
|
ON prj.id = bl.project_id AND prj.level = 'project'
|
||||||
|
|
||||||
|
-- программа через проект
|
||||||
|
LEFT JOIN v3.form4_project prog_via_prj
|
||||||
|
ON prog_via_prj.id = prj.parent_id AND prog_via_prj.level = 'program'
|
||||||
|
|
||||||
|
-- программа напрямую (bl.project_id → program)
|
||||||
|
LEFT JOIN v3.form4_project prog_direct
|
||||||
|
ON prog_direct.id = bl.project_id AND prog_direct.level = 'program'
|
||||||
|
|
||||||
|
-- раздел depth=0 (через expense_item)
|
||||||
|
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
WITH RECURSIVE up AS (
|
||||||
|
SELECT id, parent_id, name, section_code, depth
|
||||||
|
FROM v3.expense_item WHERE id = ei.id
|
||||||
|
UNION ALL
|
||||||
|
SELECT e.id, e.parent_id, e.name, e.section_code, e.depth
|
||||||
|
FROM v3.expense_item e JOIN up u ON u.parent_id = e.id
|
||||||
|
)
|
||||||
|
SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1
|
||||||
|
) sec ON TRUE
|
||||||
|
),
|
||||||
|
tp_agg AS (
|
||||||
|
SELECT 1 AS tp_id,
|
||||||
|
jsonb_build_object(
|
||||||
|
'q1', 0, 'q2', 0,
|
||||||
|
'q3', 0, 'q4', 0,
|
||||||
|
'year', 0, 'comment', null
|
||||||
|
) AS tp_plan,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'q1', 0, 'q2', 0,
|
||||||
|
'q3', 0, 'q4', 0,
|
||||||
|
'year', 0,
|
||||||
|
'justification', null
|
||||||
|
) AS tp_seq_dfip,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'q1', 0, 'q2', 0,
|
||||||
|
'q3', 0, 'q4', 0,
|
||||||
|
'year', 0
|
||||||
|
) AS tp_approved,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'total', null,
|
||||||
|
'reference', null,
|
||||||
|
'counterparty', null,
|
||||||
|
'deadline', null,
|
||||||
|
'comment', null,
|
||||||
|
'future_y1', null,
|
||||||
|
'future_y2', null,
|
||||||
|
'other_ssp', null,
|
||||||
|
'centralized_flag', null
|
||||||
|
) AS tp_contract_summary,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'contract_ref', null,
|
||||||
|
'allocation_purpose', null
|
||||||
|
) AS tp_allocation,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'q1', 0, 'q2', 0,
|
||||||
|
'q3', 0, 'q4', 0,
|
||||||
|
'year', 0,
|
||||||
|
'justification', null
|
||||||
|
) AS tp_reserve,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'approved', null,
|
||||||
|
'protocol', null,
|
||||||
|
'note', null
|
||||||
|
) AS tp_collegial,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'ceiling', null,
|
||||||
|
'q1', null,
|
||||||
|
'q2', null,
|
||||||
|
'q3', null,
|
||||||
|
'q4', null,
|
||||||
|
'rf_schedule', null,
|
||||||
|
'delivery_deadline', null,
|
||||||
|
'proc_plan', null,
|
||||||
|
'proc_method', null,
|
||||||
|
'comment', null
|
||||||
|
) AS tp_ckk,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'act', null,
|
||||||
|
'date', null,
|
||||||
|
'scheme', null,
|
||||||
|
'addenda', null,
|
||||||
|
'ceiling', null,
|
||||||
|
'comment', null,
|
||||||
|
'subject', null,
|
||||||
|
'currency', null,
|
||||||
|
'deadline', null,
|
||||||
|
'vat_rate', null,
|
||||||
|
'reference', null,
|
||||||
|
'q1', null,
|
||||||
|
'q2', null,
|
||||||
|
'q3', null,
|
||||||
|
'q4', null,
|
||||||
|
'rf_schedule', null,
|
||||||
|
'counterparty', null,
|
||||||
|
'exchange_rate', null,
|
||||||
|
'amount_foreign', null
|
||||||
|
) AS tp_contract,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'y2026', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null),
|
||||||
|
'y2027', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null)
|
||||||
|
) AS tp_booking,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'total', 0,
|
||||||
|
'adj_rf', null,
|
||||||
|
'pay_ho', null,
|
||||||
|
'pay_rf', null,
|
||||||
|
'adj_ssp', null,
|
||||||
|
'booking', null,
|
||||||
|
'pay_act', null,
|
||||||
|
'pay_date', null,
|
||||||
|
'actual_m1', null,
|
||||||
|
'actual_m2', null,
|
||||||
|
'actual_m3', null,
|
||||||
|
'pay_amount', null,
|
||||||
|
'adj_comment', null,
|
||||||
|
'adj_current', null,
|
||||||
|
'adj_reserve', null,
|
||||||
|
'pay_comment', null,
|
||||||
|
'transfer_q2', null,
|
||||||
|
'transfer_q3', null,
|
||||||
|
'transfer_q4', null,
|
||||||
|
'transfer_econ', null,
|
||||||
|
'actual_quarter', 0,
|
||||||
|
'corrected_plan', 0,
|
||||||
|
'residual_after_actual', 0,
|
||||||
|
'residual_after_booking', 0
|
||||||
|
) AS tp_q1,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'target_change', null,
|
||||||
|
'base_correction', null,
|
||||||
|
'base_correction_comment', null,
|
||||||
|
'revision_inc', null,
|
||||||
|
'revision_seq', null,
|
||||||
|
'revision_comment', null,
|
||||||
|
'new_plan', 0,
|
||||||
|
'adj_current', null,
|
||||||
|
'adj_ssp', null,
|
||||||
|
'adj_rf', null,
|
||||||
|
'adj_reserve', null,
|
||||||
|
'adj_comment', null,
|
||||||
|
'corrected_plan', 0,
|
||||||
|
'pay_date', null,
|
||||||
|
'pay_amount', null,
|
||||||
|
'pay_ho', null,
|
||||||
|
'pay_rf', null,
|
||||||
|
'pay_comment', null,
|
||||||
|
'pay_act', null,
|
||||||
|
'booking', null,
|
||||||
|
'actual_m1', null,
|
||||||
|
'actual_m2', null,
|
||||||
|
'actual_m3', null,
|
||||||
|
'actual_quarter', 0,
|
||||||
|
'residual_after_booking', 0,
|
||||||
|
'residual_after_actual', 0,
|
||||||
|
'transfer_q3', null,
|
||||||
|
'transfer_q4', null,
|
||||||
|
'transfer_econ', null,
|
||||||
|
'total', 0
|
||||||
|
) AS tp_q2,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'target_change', null,
|
||||||
|
'base_correction', null,
|
||||||
|
'base_correction_comment', null,
|
||||||
|
'revision_inc', null,
|
||||||
|
'revision_seq', null,
|
||||||
|
'revision_comment', null,
|
||||||
|
'new_plan', 0,
|
||||||
|
'adj_current', null,
|
||||||
|
'adj_ssp', null,
|
||||||
|
'adj_rf', null,
|
||||||
|
'adj_reserve', null,
|
||||||
|
'adj_comment', null,
|
||||||
|
'corrected_plan', 0,
|
||||||
|
'pay_date', null,
|
||||||
|
'pay_amount', null,
|
||||||
|
'pay_ho', null,
|
||||||
|
'pay_rf', null,
|
||||||
|
'pay_comment', null,
|
||||||
|
'pay_act', null,
|
||||||
|
'booking', null,
|
||||||
|
'actual_m1', null,
|
||||||
|
'actual_m2', null,
|
||||||
|
'actual_m3', null,
|
||||||
|
'actual_quarter', 0,
|
||||||
|
'residual_after_booking', 0,
|
||||||
|
'residual_after_actual', 0,
|
||||||
|
'transfer_q4', null,
|
||||||
|
'transfer_econ', null,
|
||||||
|
'total', 0
|
||||||
|
) AS tp_q3,
|
||||||
|
|
||||||
|
jsonb_build_object(
|
||||||
|
'target_change', null,
|
||||||
|
'base_correction', null,
|
||||||
|
'base_correction_comment', null,
|
||||||
|
'revision_inc', null,
|
||||||
|
'revision_seq', null,
|
||||||
|
'revision_comment', null,
|
||||||
|
'new_plan', 0,
|
||||||
|
'adj_current', null,
|
||||||
|
'adj_ssp', null,
|
||||||
|
'adj_rf', null,
|
||||||
|
'adj_reserve', null,
|
||||||
|
'adj_comment', null,
|
||||||
|
'corrected_plan', 0,
|
||||||
|
'pay_date', null,
|
||||||
|
'pay_amount', null,
|
||||||
|
'pay_ho', null,
|
||||||
|
'pay_rf', null,
|
||||||
|
'pay_comment', null,
|
||||||
|
'pay_act', null,
|
||||||
|
'booking', null,
|
||||||
|
'actual_m1', null,
|
||||||
|
'actual_m2', null,
|
||||||
|
'actual_m3', null,
|
||||||
|
'actual_spod', null,
|
||||||
|
'actual_quarter', 0,
|
||||||
|
'residual_after_booking', 0,
|
||||||
|
'residual_after_actual', 0,
|
||||||
|
'transfer_econ', null,
|
||||||
|
'total', 0
|
||||||
|
) AS tp_q4,
|
||||||
|
jsonb_build_object(
|
||||||
|
'fact_year', 0
|
||||||
|
) AS tp_totals
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
),
|
||||||
|
-- ═══ 2. Все программы (включая пустые) ═════════════════════════════════════
|
||||||
|
all_programs AS (
|
||||||
|
SELECT
|
||||||
|
fp.id AS prog_id,
|
||||||
|
fp.name AS prog_name,
|
||||||
|
fp.section_code,
|
||||||
|
sec.id AS sec_id,
|
||||||
|
sec.section_code AS sec_code,
|
||||||
|
sec.name AS sec_name
|
||||||
|
FROM v3.form4_project fp
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT id, section_code, name
|
||||||
|
FROM v3.expense_item
|
||||||
|
WHERE section_code = fp.section_code AND depth = 0
|
||||||
|
LIMIT 1
|
||||||
|
) sec ON TRUE
|
||||||
|
WHERE fp.level = 'program'
|
||||||
|
AND (p_sections IS NULL OR fp.section_code = ANY(p_sections))
|
||||||
|
AND (fp.section_code = p_sheet) AND fp.form_id = p_form_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 3. Все проекты под программами (включая пустые) ═══════════════════════
|
||||||
|
all_projects AS (
|
||||||
|
SELECT
|
||||||
|
prj.id AS prj_id,
|
||||||
|
prj.name AS prj_name,
|
||||||
|
prj.parent_id AS prog_id,
|
||||||
|
ap.sec_id, ap.sec_code, ap.sec_name
|
||||||
|
FROM v3.form4_project prj
|
||||||
|
JOIN all_programs ap ON ap.prog_id = prj.parent_id
|
||||||
|
WHERE prj.level = 'project' AND (ap.section_code = p_sheet)
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 4. Агрегаты с финансовыми данными (только из inp) ════════════════════
|
||||||
|
-- Замена рекурсивного агрегата v3.jsonb_sum (AURORA-1846): листья jsonb
|
||||||
|
-- разворачиваются в строки, суммируются GROUPING SETS по трём уровням
|
||||||
|
-- (раздел / программа / проект) и собираются обратно в jsonb.
|
||||||
|
-- Правила те же, что у v3.jsonb_numeric_sum: числа складываются, нечисловые
|
||||||
|
-- игнорируются, если по пути есть число; текст/null сохраняются, если путь
|
||||||
|
-- встретился в группе один раз; иначе JSON null; NULL-секция даёт NULL.
|
||||||
|
|
||||||
|
-- Имена групп (без jsonb)
|
||||||
|
prj_names AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
prog_id, MAX(prog_name) AS prog_name,
|
||||||
|
prj_id, MAX(prj_name) AS prj_name
|
||||||
|
FROM inp WHERE prj_id IS NOT NULL
|
||||||
|
GROUP BY sec_id, prog_id, prj_id
|
||||||
|
),
|
||||||
|
prog_names AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
prog_id, MAX(prog_name) AS prog_name
|
||||||
|
FROM inp WHERE prog_id IS NOT NULL
|
||||||
|
GROUP BY sec_id, prog_id
|
||||||
|
),
|
||||||
|
root_names AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name
|
||||||
|
FROM inp
|
||||||
|
GROUP BY sec_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Листья jsonb: (колонка, ключ 1-го уровня, ключ 2-го уровня или NULL, значение)
|
||||||
|
leaves AS (
|
||||||
|
SELECT
|
||||||
|
i.sec_id, i.prog_id, i.prj_id,
|
||||||
|
c.col, e1.key AS k1, e2.key AS k2,
|
||||||
|
COALESCE(e2.value, e1.value) AS val
|
||||||
|
FROM inp i
|
||||||
|
CROSS JOIN LATERAL (VALUES
|
||||||
|
('plan', i.plan_data),
|
||||||
|
('seq_dfip', i.seq_dfip_data),
|
||||||
|
('approved', i.approved_data),
|
||||||
|
('contract_summary', i.contract_summary_data),
|
||||||
|
('allocation', i.allocation_data),
|
||||||
|
('reserve', i.reserve_data),
|
||||||
|
('collegial', i.collegial_data),
|
||||||
|
('ckk', i.ckk_data),
|
||||||
|
('contract', i.contract_data),
|
||||||
|
('booking', i.booking_data),
|
||||||
|
('q1', i.q1_data),
|
||||||
|
('q2', i.q2_data),
|
||||||
|
('q3', i.q3_data),
|
||||||
|
('q4', i.q4_data),
|
||||||
|
('totals', i.totals_data)
|
||||||
|
) AS c(col, v)
|
||||||
|
CROSS JOIN LATERAL jsonb_each(
|
||||||
|
CASE WHEN jsonb_typeof(c.v) = 'object' THEN c.v END
|
||||||
|
) AS e1
|
||||||
|
LEFT JOIN LATERAL jsonb_each(
|
||||||
|
CASE WHEN jsonb_typeof(e1.value) = 'object' THEN e1.value END
|
||||||
|
) AS e2 ON TRUE
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Суммы листьев сразу по трём уровням (lvl: 3 раздел, 1 программа, 0 проект)
|
||||||
|
leaf_agg AS (
|
||||||
|
SELECT
|
||||||
|
GROUPING(prog_id, prj_id) AS lvl,
|
||||||
|
sec_id, prog_id, prj_id, col, k1, k2,
|
||||||
|
COUNT(*) AS n,
|
||||||
|
COUNT(*) FILTER (WHERE jsonb_typeof(val) = 'number') AS n_num,
|
||||||
|
SUM((val #>> '{}')::numeric) FILTER (WHERE jsonb_typeof(val) = 'number') AS s,
|
||||||
|
(array_agg(val))[1] AS one
|
||||||
|
FROM leaves
|
||||||
|
GROUP BY GROUPING SETS (
|
||||||
|
(sec_id, col, k1, k2),
|
||||||
|
(sec_id, prog_id, col, k1, k2),
|
||||||
|
(sec_id, prog_id, prj_id, col, k1, k2)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
leaf_val AS (
|
||||||
|
SELECT
|
||||||
|
lvl, sec_id, prog_id, prj_id, col, k1, k2,
|
||||||
|
CASE
|
||||||
|
WHEN n_num > 0 THEN to_jsonb(s)
|
||||||
|
WHEN n = 1 THEN one
|
||||||
|
ELSE 'null'::jsonb
|
||||||
|
END AS v
|
||||||
|
FROM leaf_agg
|
||||||
|
WHERE NOT (lvl = 1 AND prog_id IS NULL)
|
||||||
|
AND NOT (lvl = 0 AND prj_id IS NULL)
|
||||||
|
),
|
||||||
|
-- Сборка второго уровня вложенности (booking.y2026.q1 → объект по ключу k1)
|
||||||
|
k1_val AS (
|
||||||
|
SELECT
|
||||||
|
lvl, sec_id, prog_id, prj_id, col, k1,
|
||||||
|
CASE
|
||||||
|
WHEN bool_or(k2 IS NOT NULL) THEN jsonb_object_agg(k2, v) FILTER (WHERE k2 IS NOT NULL)
|
||||||
|
ELSE (array_agg(v))[1]
|
||||||
|
END AS v
|
||||||
|
FROM leaf_val
|
||||||
|
GROUP BY lvl, sec_id, prog_id, prj_id, col, k1
|
||||||
|
),
|
||||||
|
-- Сборка по колонкам: одна строка на группу
|
||||||
|
lvl_data AS (
|
||||||
|
SELECT
|
||||||
|
lvl, sec_id, prog_id, prj_id,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'plan') AS plan_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'seq_dfip') AS seq_dfip_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'approved') AS approved_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'contract_summary') AS contract_summary_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'allocation') AS allocation_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'reserve') AS reserve_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'collegial') AS collegial_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'ckk') AS ckk_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'contract') AS contract_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'booking') AS booking_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q1') AS q1_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q2') AS q2_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q3') AS q3_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q4') AS q4_data,
|
||||||
|
jsonb_object_agg(k1, v) FILTER (WHERE col = 'totals') AS totals_data
|
||||||
|
FROM k1_val
|
||||||
|
GROUP BY lvl, sec_id, prog_id, prj_id
|
||||||
|
),
|
||||||
|
|
||||||
|
prj_agg AS (
|
||||||
|
SELECT n.*, d.plan_data, d.seq_dfip_data, d.approved_data, d.contract_summary_data, d.allocation_data, d.reserve_data, d.collegial_data, d.ckk_data, d.contract_data, d.booking_data, d.q1_data, d.q2_data, d.q3_data, d.q4_data, d.totals_data
|
||||||
|
FROM prj_names n
|
||||||
|
LEFT JOIN lvl_data d
|
||||||
|
ON d.lvl = 0 AND d.sec_id = n.sec_id AND d.prog_id = n.prog_id AND d.prj_id = n.prj_id
|
||||||
|
),
|
||||||
|
prog_agg AS (
|
||||||
|
SELECT n.*, d.plan_data, d.seq_dfip_data, d.approved_data, d.contract_summary_data, d.allocation_data, d.reserve_data, d.collegial_data, d.ckk_data, d.contract_data, d.booking_data, d.q1_data, d.q2_data, d.q3_data, d.q4_data, d.totals_data
|
||||||
|
FROM prog_names n
|
||||||
|
LEFT JOIN lvl_data d
|
||||||
|
ON d.lvl = 1 AND d.sec_id = n.sec_id AND d.prog_id = n.prog_id
|
||||||
|
),
|
||||||
|
root_agg AS (
|
||||||
|
SELECT n.*, d.plan_data, d.seq_dfip_data, d.approved_data, d.contract_summary_data, d.allocation_data, d.reserve_data, d.collegial_data, d.ckk_data, d.contract_data, d.booking_data, d.q1_data, d.q2_data, d.q3_data, d.q4_data, d.totals_data
|
||||||
|
FROM root_names n
|
||||||
|
LEFT JOIN lvl_data d
|
||||||
|
ON d.lvl = 3 AND d.sec_id = n.sec_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Нулевой JSONB-шаблон (для пустых программ/проектов)
|
||||||
|
zero AS (
|
||||||
|
SELECT '{}'::jsonb AS z
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 5. UNION всех уровней ═════════════════════════════════════════════════
|
||||||
|
unioned AS (
|
||||||
|
-- ROOT — только из агрегатов (не имеет смысла пустой ROOT без строк)
|
||||||
|
SELECT
|
||||||
|
'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', sec_name, 'expense_item_id', sec_id) AS header,
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path
|
||||||
|
FROM root_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- GROUP — из агрегатов (программы с бюджетными строками)
|
||||||
|
SELECT
|
||||||
|
'GROUP', 1, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
||||||
|
FROM prog_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- GROUP — пустые программы (нет ни одной budget_line)
|
||||||
|
SELECT
|
||||||
|
'GROUP', 1, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
||||||
|
tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved
|
||||||
|
tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve
|
||||||
|
tp_collegial, tp_ckk, -- collegial, ckk
|
||||||
|
tp_contract, tp_booking, -- contract, booking
|
||||||
|
tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals
|
||||||
|
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
||||||
|
FROM all_programs ap
|
||||||
|
JOIN tp_agg ON tp_agg.tp_id = 1
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_line bl
|
||||||
|
WHERE bl.budget_form_id = p_form_id
|
||||||
|
AND (
|
||||||
|
bl.project_id = ap.prog_id -- прямая привязка к программе
|
||||||
|
OR bl.project_id IN (SELECT id FROM v3.form4_project pr -- через проект
|
||||||
|
WHERE pr.parent_id = ap.prog_id AND pr.level = 'project')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- ITEM — из агрегатов (проекты с бюджетными строками)
|
||||||
|
SELECT
|
||||||
|
'ITEM', 2, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
||||||
|
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
||||||
|
FROM prj_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- ITEM — пустые проекты (нет budget_line)
|
||||||
|
SELECT
|
||||||
|
'ITEM', 2, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
||||||
|
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
||||||
|
tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved
|
||||||
|
tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve
|
||||||
|
tp_collegial, tp_ckk, -- collegial, ckk
|
||||||
|
tp_contract, tp_booking, -- contract, booking
|
||||||
|
tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals
|
||||||
|
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
||||||
|
FROM all_projects apr
|
||||||
|
JOIN tp_agg ON tp_agg.tp_id = 1
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_line bl
|
||||||
|
WHERE bl.budget_form_id = p_form_id
|
||||||
|
AND bl.project_id = apr.prj_id
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- INPUT — все строки бюджета
|
||||||
|
SELECT
|
||||||
|
'INPUT', 3, line_id,
|
||||||
|
header || jsonb_build_object(
|
||||||
|
'project_id', prj_id, 'project_name', prj_name,
|
||||||
|
'program_id', prog_id, 'program_name', prog_name
|
||||||
|
),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(CASE WHEN prj_id is null and prog_id is not null THEN 0 ELSE prj_id END, 999999), line_id]::INT[]
|
||||||
|
FROM inp
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
u.row_type, u.depth,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order,
|
||||||
|
jsonb_build_object(
|
||||||
|
'sort_path', u.sort_path,
|
||||||
|
'line_id', u.line_id,
|
||||||
|
'header', u.header,
|
||||||
|
'plan', u.plan_data,
|
||||||
|
'seq_dfip', u.seq_dfip_data,
|
||||||
|
'approved', u.approved_data,
|
||||||
|
'contract_summary', u.contract_summary_data,
|
||||||
|
'allocation', u.allocation_data,
|
||||||
|
'reserve', u.reserve_data,
|
||||||
|
'collegial', u.collegial_data,
|
||||||
|
'ckk', u.ckk_data,
|
||||||
|
'contract', u.contract_data,
|
||||||
|
'booking', u.booking_data,
|
||||||
|
'q1', u.q1_data,
|
||||||
|
'q2', u.q2_data,
|
||||||
|
'q3', u.q3_data,
|
||||||
|
'q4', u.q4_data,
|
||||||
|
'totals', u.totals_data
|
||||||
|
) AS data
|
||||||
|
FROM unioned u
|
||||||
|
ORDER BY u.sort_path;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
|||||||
@ -1,603 +0,0 @@
|
|||||||
-- AURORA-1846: FORM_4, замена рекурсивного агрегата v3.jsonb_sum в
|
|
||||||
-- v3.v_form4_sheet_jsonb на разворот листьев jsonb + GROUPING SETS.
|
|
||||||
-- Тело — копия sql/0012_del_project.sql:1091-1617, изменён только блок
|
|
||||||
-- «4. Агрегаты». v3.jsonb_sum / v3.jsonb_numeric_sum не удаляются (эталон в тестах).
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
|
|
||||||
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
|
||||||
LANGUAGE sql
|
|
||||||
STABLE
|
|
||||||
SET jit TO 'off' -- оценка плана выше порога JIT, компиляция дороже выполнения (AURORA-1846)
|
|
||||||
AS $function$
|
|
||||||
-- ═══ 1. Базовые INPUT-строки с расширенной иерархией ═══════════════════════
|
|
||||||
WITH inp_raw AS (
|
|
||||||
SELECT j.*
|
|
||||||
FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j
|
|
||||||
WHERE j.row_type = 'INPUT'
|
|
||||||
),
|
|
||||||
inp AS (
|
|
||||||
SELECT
|
|
||||||
i.*,
|
|
||||||
bl.project_id AS line_project_id,
|
|
||||||
|
|
||||||
-- --- разрешение проекта ---
|
|
||||||
-- A. bl.project_id → project (level='project')
|
|
||||||
-- B. bl.project_id → program (level='program') — тогда проекта нет
|
|
||||||
CASE WHEN prj.id IS NOT NULL THEN prj.id END AS prj_id,
|
|
||||||
CASE WHEN prj.id IS NOT NULL THEN prj.name END AS prj_name,
|
|
||||||
|
|
||||||
-- --- разрешение программы ---
|
|
||||||
-- A. через проект: prog_via_prj
|
|
||||||
-- B. напрямую: prog_direct
|
|
||||||
COALESCE(prog_via_prj.id, prog_direct.id) AS prog_id,
|
|
||||||
COALESCE(prog_via_prj.name, prog_direct.name) AS prog_name,
|
|
||||||
|
|
||||||
-- --- section (depth=0) ---
|
|
||||||
sec.id AS sec_id,
|
|
||||||
sec.section_code AS sec_code,
|
|
||||||
sec.name AS sec_name
|
|
||||||
|
|
||||||
FROM inp_raw i
|
|
||||||
JOIN v3.budget_line bl ON bl.id = i.line_id
|
|
||||||
|
|
||||||
-- проект (level='project')
|
|
||||||
LEFT JOIN v3.form4_project prj
|
|
||||||
ON prj.id = bl.project_id AND prj.level = 'project'
|
|
||||||
|
|
||||||
-- программа через проект
|
|
||||||
LEFT JOIN v3.form4_project prog_via_prj
|
|
||||||
ON prog_via_prj.id = prj.parent_id AND prog_via_prj.level = 'program'
|
|
||||||
|
|
||||||
-- программа напрямую (bl.project_id → program)
|
|
||||||
LEFT JOIN v3.form4_project prog_direct
|
|
||||||
ON prog_direct.id = bl.project_id AND prog_direct.level = 'program'
|
|
||||||
|
|
||||||
-- раздел depth=0 (через expense_item)
|
|
||||||
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
|
||||||
LEFT JOIN LATERAL (
|
|
||||||
WITH RECURSIVE up AS (
|
|
||||||
SELECT id, parent_id, name, section_code, depth
|
|
||||||
FROM v3.expense_item WHERE id = ei.id
|
|
||||||
UNION ALL
|
|
||||||
SELECT e.id, e.parent_id, e.name, e.section_code, e.depth
|
|
||||||
FROM v3.expense_item e JOIN up u ON u.parent_id = e.id
|
|
||||||
)
|
|
||||||
SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1
|
|
||||||
) sec ON TRUE
|
|
||||||
),
|
|
||||||
tp_agg AS (
|
|
||||||
SELECT 1 AS tp_id,
|
|
||||||
jsonb_build_object(
|
|
||||||
'q1', 0, 'q2', 0,
|
|
||||||
'q3', 0, 'q4', 0,
|
|
||||||
'year', 0, 'comment', null
|
|
||||||
) AS tp_plan,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'q1', 0, 'q2', 0,
|
|
||||||
'q3', 0, 'q4', 0,
|
|
||||||
'year', 0,
|
|
||||||
'justification', null
|
|
||||||
) AS tp_seq_dfip,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'q1', 0, 'q2', 0,
|
|
||||||
'q3', 0, 'q4', 0,
|
|
||||||
'year', 0
|
|
||||||
) AS tp_approved,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'total', null,
|
|
||||||
'reference', null,
|
|
||||||
'counterparty', null,
|
|
||||||
'deadline', null,
|
|
||||||
'comment', null,
|
|
||||||
'future_y1', null,
|
|
||||||
'future_y2', null,
|
|
||||||
'other_ssp', null,
|
|
||||||
'centralized_flag', null
|
|
||||||
) AS tp_contract_summary,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'contract_ref', null,
|
|
||||||
'allocation_purpose', null
|
|
||||||
) AS tp_allocation,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'q1', 0, 'q2', 0,
|
|
||||||
'q3', 0, 'q4', 0,
|
|
||||||
'year', 0,
|
|
||||||
'justification', null
|
|
||||||
) AS tp_reserve,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'approved', null,
|
|
||||||
'protocol', null,
|
|
||||||
'note', null
|
|
||||||
) AS tp_collegial,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'ceiling', null,
|
|
||||||
'q1', null,
|
|
||||||
'q2', null,
|
|
||||||
'q3', null,
|
|
||||||
'q4', null,
|
|
||||||
'rf_schedule', null,
|
|
||||||
'delivery_deadline', null,
|
|
||||||
'proc_plan', null,
|
|
||||||
'proc_method', null,
|
|
||||||
'comment', null
|
|
||||||
) AS tp_ckk,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'act', null,
|
|
||||||
'date', null,
|
|
||||||
'scheme', null,
|
|
||||||
'addenda', null,
|
|
||||||
'ceiling', null,
|
|
||||||
'comment', null,
|
|
||||||
'subject', null,
|
|
||||||
'currency', null,
|
|
||||||
'deadline', null,
|
|
||||||
'vat_rate', null,
|
|
||||||
'reference', null,
|
|
||||||
'q1', null,
|
|
||||||
'q2', null,
|
|
||||||
'q3', null,
|
|
||||||
'q4', null,
|
|
||||||
'rf_schedule', null,
|
|
||||||
'counterparty', null,
|
|
||||||
'exchange_rate', null,
|
|
||||||
'amount_foreign', null
|
|
||||||
) AS tp_contract,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'y2026', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null),
|
|
||||||
'y2027', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null)
|
|
||||||
) AS tp_booking,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'total', 0,
|
|
||||||
'adj_rf', null,
|
|
||||||
'pay_ho', null,
|
|
||||||
'pay_rf', null,
|
|
||||||
'adj_ssp', null,
|
|
||||||
'booking', null,
|
|
||||||
'pay_act', null,
|
|
||||||
'pay_date', null,
|
|
||||||
'actual_m1', null,
|
|
||||||
'actual_m2', null,
|
|
||||||
'actual_m3', null,
|
|
||||||
'pay_amount', null,
|
|
||||||
'adj_comment', null,
|
|
||||||
'adj_current', null,
|
|
||||||
'adj_reserve', null,
|
|
||||||
'pay_comment', null,
|
|
||||||
'transfer_q2', null,
|
|
||||||
'transfer_q3', null,
|
|
||||||
'transfer_q4', null,
|
|
||||||
'transfer_econ', null,
|
|
||||||
'actual_quarter', 0,
|
|
||||||
'corrected_plan', 0,
|
|
||||||
'residual_after_actual', 0,
|
|
||||||
'residual_after_booking', 0
|
|
||||||
) AS tp_q1,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'target_change', null,
|
|
||||||
'base_correction', null,
|
|
||||||
'base_correction_comment', null,
|
|
||||||
'revision_inc', null,
|
|
||||||
'revision_seq', null,
|
|
||||||
'revision_comment', null,
|
|
||||||
'new_plan', 0,
|
|
||||||
'adj_current', null,
|
|
||||||
'adj_ssp', null,
|
|
||||||
'adj_rf', null,
|
|
||||||
'adj_reserve', null,
|
|
||||||
'adj_comment', null,
|
|
||||||
'corrected_plan', 0,
|
|
||||||
'pay_date', null,
|
|
||||||
'pay_amount', null,
|
|
||||||
'pay_ho', null,
|
|
||||||
'pay_rf', null,
|
|
||||||
'pay_comment', null,
|
|
||||||
'pay_act', null,
|
|
||||||
'booking', null,
|
|
||||||
'actual_m1', null,
|
|
||||||
'actual_m2', null,
|
|
||||||
'actual_m3', null,
|
|
||||||
'actual_quarter', 0,
|
|
||||||
'residual_after_booking', 0,
|
|
||||||
'residual_after_actual', 0,
|
|
||||||
'transfer_q3', null,
|
|
||||||
'transfer_q4', null,
|
|
||||||
'transfer_econ', null,
|
|
||||||
'total', 0
|
|
||||||
) AS tp_q2,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'target_change', null,
|
|
||||||
'base_correction', null,
|
|
||||||
'base_correction_comment', null,
|
|
||||||
'revision_inc', null,
|
|
||||||
'revision_seq', null,
|
|
||||||
'revision_comment', null,
|
|
||||||
'new_plan', 0,
|
|
||||||
'adj_current', null,
|
|
||||||
'adj_ssp', null,
|
|
||||||
'adj_rf', null,
|
|
||||||
'adj_reserve', null,
|
|
||||||
'adj_comment', null,
|
|
||||||
'corrected_plan', 0,
|
|
||||||
'pay_date', null,
|
|
||||||
'pay_amount', null,
|
|
||||||
'pay_ho', null,
|
|
||||||
'pay_rf', null,
|
|
||||||
'pay_comment', null,
|
|
||||||
'pay_act', null,
|
|
||||||
'booking', null,
|
|
||||||
'actual_m1', null,
|
|
||||||
'actual_m2', null,
|
|
||||||
'actual_m3', null,
|
|
||||||
'actual_quarter', 0,
|
|
||||||
'residual_after_booking', 0,
|
|
||||||
'residual_after_actual', 0,
|
|
||||||
'transfer_q4', null,
|
|
||||||
'transfer_econ', null,
|
|
||||||
'total', 0
|
|
||||||
) AS tp_q3,
|
|
||||||
|
|
||||||
jsonb_build_object(
|
|
||||||
'target_change', null,
|
|
||||||
'base_correction', null,
|
|
||||||
'base_correction_comment', null,
|
|
||||||
'revision_inc', null,
|
|
||||||
'revision_seq', null,
|
|
||||||
'revision_comment', null,
|
|
||||||
'new_plan', 0,
|
|
||||||
'adj_current', null,
|
|
||||||
'adj_ssp', null,
|
|
||||||
'adj_rf', null,
|
|
||||||
'adj_reserve', null,
|
|
||||||
'adj_comment', null,
|
|
||||||
'corrected_plan', 0,
|
|
||||||
'pay_date', null,
|
|
||||||
'pay_amount', null,
|
|
||||||
'pay_ho', null,
|
|
||||||
'pay_rf', null,
|
|
||||||
'pay_comment', null,
|
|
||||||
'pay_act', null,
|
|
||||||
'booking', null,
|
|
||||||
'actual_m1', null,
|
|
||||||
'actual_m2', null,
|
|
||||||
'actual_m3', null,
|
|
||||||
'actual_spod', null,
|
|
||||||
'actual_quarter', 0,
|
|
||||||
'residual_after_booking', 0,
|
|
||||||
'residual_after_actual', 0,
|
|
||||||
'transfer_econ', null,
|
|
||||||
'total', 0
|
|
||||||
) AS tp_q4,
|
|
||||||
jsonb_build_object(
|
|
||||||
'fact_year', 0
|
|
||||||
) AS tp_totals
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
),
|
|
||||||
-- ═══ 2. Все программы (включая пустые) ═════════════════════════════════════
|
|
||||||
all_programs AS (
|
|
||||||
SELECT
|
|
||||||
fp.id AS prog_id,
|
|
||||||
fp.name AS prog_name,
|
|
||||||
fp.section_code,
|
|
||||||
sec.id AS sec_id,
|
|
||||||
sec.section_code AS sec_code,
|
|
||||||
sec.name AS sec_name
|
|
||||||
FROM v3.form4_project fp
|
|
||||||
LEFT JOIN LATERAL (
|
|
||||||
SELECT id, section_code, name
|
|
||||||
FROM v3.expense_item
|
|
||||||
WHERE section_code = fp.section_code AND depth = 0
|
|
||||||
LIMIT 1
|
|
||||||
) sec ON TRUE
|
|
||||||
WHERE fp.level = 'program'
|
|
||||||
AND (p_sections IS NULL OR fp.section_code = ANY(p_sections))
|
|
||||||
AND (fp.section_code = p_sheet) AND fp.form_id = p_form_id
|
|
||||||
),
|
|
||||||
|
|
||||||
-- ═══ 3. Все проекты под программами (включая пустые) ═══════════════════════
|
|
||||||
all_projects AS (
|
|
||||||
SELECT
|
|
||||||
prj.id AS prj_id,
|
|
||||||
prj.name AS prj_name,
|
|
||||||
prj.parent_id AS prog_id,
|
|
||||||
ap.sec_id, ap.sec_code, ap.sec_name
|
|
||||||
FROM v3.form4_project prj
|
|
||||||
JOIN all_programs ap ON ap.prog_id = prj.parent_id
|
|
||||||
WHERE prj.level = 'project' AND (ap.section_code = p_sheet)
|
|
||||||
),
|
|
||||||
|
|
||||||
-- ═══ 4. Агрегаты с финансовыми данными (только из inp) ════════════════════
|
|
||||||
-- Замена рекурсивного агрегата v3.jsonb_sum (AURORA-1846): листья jsonb
|
|
||||||
-- разворачиваются в строки, суммируются GROUPING SETS по трём уровням
|
|
||||||
-- (раздел / программа / проект) и собираются обратно в jsonb.
|
|
||||||
-- Правила те же, что у v3.jsonb_numeric_sum: числа складываются, нечисловые
|
|
||||||
-- игнорируются, если по пути есть число; текст/null сохраняются, если путь
|
|
||||||
-- встретился в группе один раз; иначе JSON null; NULL-секция даёт NULL.
|
|
||||||
|
|
||||||
-- Имена групп (без jsonb)
|
|
||||||
prj_names AS (
|
|
||||||
SELECT
|
|
||||||
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
|
||||||
prog_id, MAX(prog_name) AS prog_name,
|
|
||||||
prj_id, MAX(prj_name) AS prj_name
|
|
||||||
FROM inp WHERE prj_id IS NOT NULL
|
|
||||||
GROUP BY sec_id, prog_id, prj_id
|
|
||||||
),
|
|
||||||
prog_names AS (
|
|
||||||
SELECT
|
|
||||||
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
|
||||||
prog_id, MAX(prog_name) AS prog_name
|
|
||||||
FROM inp WHERE prog_id IS NOT NULL
|
|
||||||
GROUP BY sec_id, prog_id
|
|
||||||
),
|
|
||||||
root_names AS (
|
|
||||||
SELECT
|
|
||||||
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name
|
|
||||||
FROM inp
|
|
||||||
GROUP BY sec_id
|
|
||||||
),
|
|
||||||
|
|
||||||
-- Листья jsonb: (колонка, ключ 1-го уровня, ключ 2-го уровня или NULL, значение)
|
|
||||||
leaves AS (
|
|
||||||
SELECT
|
|
||||||
i.sec_id, i.prog_id, i.prj_id,
|
|
||||||
c.col, e1.key AS k1, e2.key AS k2,
|
|
||||||
COALESCE(e2.value, e1.value) AS val
|
|
||||||
FROM inp i
|
|
||||||
CROSS JOIN LATERAL (VALUES
|
|
||||||
('plan', i.plan_data),
|
|
||||||
('seq_dfip', i.seq_dfip_data),
|
|
||||||
('approved', i.approved_data),
|
|
||||||
('contract_summary', i.contract_summary_data),
|
|
||||||
('allocation', i.allocation_data),
|
|
||||||
('reserve', i.reserve_data),
|
|
||||||
('collegial', i.collegial_data),
|
|
||||||
('ckk', i.ckk_data),
|
|
||||||
('contract', i.contract_data),
|
|
||||||
('booking', i.booking_data),
|
|
||||||
('q1', i.q1_data),
|
|
||||||
('q2', i.q2_data),
|
|
||||||
('q3', i.q3_data),
|
|
||||||
('q4', i.q4_data),
|
|
||||||
('totals', i.totals_data)
|
|
||||||
) AS c(col, v)
|
|
||||||
CROSS JOIN LATERAL jsonb_each(
|
|
||||||
CASE WHEN jsonb_typeof(c.v) = 'object' THEN c.v END
|
|
||||||
) AS e1
|
|
||||||
LEFT JOIN LATERAL jsonb_each(
|
|
||||||
CASE WHEN jsonb_typeof(e1.value) = 'object' THEN e1.value END
|
|
||||||
) AS e2 ON TRUE
|
|
||||||
),
|
|
||||||
|
|
||||||
-- Суммы листьев сразу по трём уровням (lvl: 3 раздел, 1 программа, 0 проект)
|
|
||||||
leaf_agg AS (
|
|
||||||
SELECT
|
|
||||||
GROUPING(prog_id, prj_id) AS lvl,
|
|
||||||
sec_id, prog_id, prj_id, col, k1, k2,
|
|
||||||
COUNT(*) AS n,
|
|
||||||
COUNT(*) FILTER (WHERE jsonb_typeof(val) = 'number') AS n_num,
|
|
||||||
SUM((val #>> '{}')::numeric) FILTER (WHERE jsonb_typeof(val) = 'number') AS s,
|
|
||||||
(array_agg(val))[1] AS one
|
|
||||||
FROM leaves
|
|
||||||
GROUP BY GROUPING SETS (
|
|
||||||
(sec_id, col, k1, k2),
|
|
||||||
(sec_id, prog_id, col, k1, k2),
|
|
||||||
(sec_id, prog_id, prj_id, col, k1, k2)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
leaf_val AS (
|
|
||||||
SELECT
|
|
||||||
lvl, sec_id, prog_id, prj_id, col, k1, k2,
|
|
||||||
CASE
|
|
||||||
WHEN n_num > 0 THEN to_jsonb(s)
|
|
||||||
WHEN n = 1 THEN one
|
|
||||||
ELSE 'null'::jsonb
|
|
||||||
END AS v
|
|
||||||
FROM leaf_agg
|
|
||||||
WHERE NOT (lvl = 1 AND prog_id IS NULL)
|
|
||||||
AND NOT (lvl = 0 AND prj_id IS NULL)
|
|
||||||
),
|
|
||||||
-- Сборка второго уровня вложенности (booking.y2026.q1 → объект по ключу k1)
|
|
||||||
k1_val AS (
|
|
||||||
SELECT
|
|
||||||
lvl, sec_id, prog_id, prj_id, col, k1,
|
|
||||||
CASE
|
|
||||||
WHEN bool_or(k2 IS NOT NULL) THEN jsonb_object_agg(k2, v) FILTER (WHERE k2 IS NOT NULL)
|
|
||||||
ELSE (array_agg(v))[1]
|
|
||||||
END AS v
|
|
||||||
FROM leaf_val
|
|
||||||
GROUP BY lvl, sec_id, prog_id, prj_id, col, k1
|
|
||||||
),
|
|
||||||
-- Сборка по колонкам: одна строка на группу
|
|
||||||
lvl_data AS (
|
|
||||||
SELECT
|
|
||||||
lvl, sec_id, prog_id, prj_id,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'plan') AS plan_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'seq_dfip') AS seq_dfip_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'approved') AS approved_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'contract_summary') AS contract_summary_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'allocation') AS allocation_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'reserve') AS reserve_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'collegial') AS collegial_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'ckk') AS ckk_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'contract') AS contract_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'booking') AS booking_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q1') AS q1_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q2') AS q2_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q3') AS q3_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'q4') AS q4_data,
|
|
||||||
jsonb_object_agg(k1, v) FILTER (WHERE col = 'totals') AS totals_data
|
|
||||||
FROM k1_val
|
|
||||||
GROUP BY lvl, sec_id, prog_id, prj_id
|
|
||||||
),
|
|
||||||
|
|
||||||
prj_agg AS (
|
|
||||||
SELECT n.*, d.plan_data, d.seq_dfip_data, d.approved_data, d.contract_summary_data, d.allocation_data, d.reserve_data, d.collegial_data, d.ckk_data, d.contract_data, d.booking_data, d.q1_data, d.q2_data, d.q3_data, d.q4_data, d.totals_data
|
|
||||||
FROM prj_names n
|
|
||||||
LEFT JOIN lvl_data d
|
|
||||||
ON d.lvl = 0 AND d.sec_id = n.sec_id AND d.prog_id = n.prog_id AND d.prj_id = n.prj_id
|
|
||||||
),
|
|
||||||
prog_agg AS (
|
|
||||||
SELECT n.*, d.plan_data, d.seq_dfip_data, d.approved_data, d.contract_summary_data, d.allocation_data, d.reserve_data, d.collegial_data, d.ckk_data, d.contract_data, d.booking_data, d.q1_data, d.q2_data, d.q3_data, d.q4_data, d.totals_data
|
|
||||||
FROM prog_names n
|
|
||||||
LEFT JOIN lvl_data d
|
|
||||||
ON d.lvl = 1 AND d.sec_id = n.sec_id AND d.prog_id = n.prog_id
|
|
||||||
),
|
|
||||||
root_agg AS (
|
|
||||||
SELECT n.*, d.plan_data, d.seq_dfip_data, d.approved_data, d.contract_summary_data, d.allocation_data, d.reserve_data, d.collegial_data, d.ckk_data, d.contract_data, d.booking_data, d.q1_data, d.q2_data, d.q3_data, d.q4_data, d.totals_data
|
|
||||||
FROM root_names n
|
|
||||||
LEFT JOIN lvl_data d
|
|
||||||
ON d.lvl = 3 AND d.sec_id = n.sec_id
|
|
||||||
),
|
|
||||||
|
|
||||||
-- Нулевой JSONB-шаблон (для пустых программ/проектов)
|
|
||||||
zero AS (
|
|
||||||
SELECT '{}'::jsonb AS z
|
|
||||||
),
|
|
||||||
|
|
||||||
-- ═══ 5. UNION всех уровней ═════════════════════════════════════════════════
|
|
||||||
unioned AS (
|
|
||||||
-- ROOT — только из агрегатов (не имеет смысла пустой ROOT без строк)
|
|
||||||
SELECT
|
|
||||||
'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id,
|
|
||||||
jsonb_build_object('section_code', sec_code, 'name', sec_name, 'expense_item_id', sec_id) AS header,
|
|
||||||
plan_data, seq_dfip_data, approved_data,
|
|
||||||
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
|
||||||
contract_data, booking_data,
|
|
||||||
q1_data, q2_data, q3_data, q4_data, totals_data,
|
|
||||||
ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path
|
|
||||||
FROM root_agg
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- GROUP — из агрегатов (программы с бюджетными строками)
|
|
||||||
SELECT
|
|
||||||
'GROUP', 1, NULL,
|
|
||||||
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
|
||||||
plan_data, seq_dfip_data, approved_data,
|
|
||||||
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
|
||||||
contract_data, booking_data,
|
|
||||||
q1_data, q2_data, q3_data, q4_data, totals_data,
|
|
||||||
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
|
||||||
FROM prog_agg
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- GROUP — пустые программы (нет ни одной budget_line)
|
|
||||||
SELECT
|
|
||||||
'GROUP', 1, NULL,
|
|
||||||
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
|
||||||
tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved
|
|
||||||
tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve
|
|
||||||
tp_collegial, tp_ckk, -- collegial, ckk
|
|
||||||
tp_contract, tp_booking, -- contract, booking
|
|
||||||
tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals
|
|
||||||
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
|
||||||
FROM all_programs ap
|
|
||||||
JOIN tp_agg ON tp_agg.tp_id = 1
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM v3.budget_line bl
|
|
||||||
WHERE bl.budget_form_id = p_form_id
|
|
||||||
AND (
|
|
||||||
bl.project_id = ap.prog_id -- прямая привязка к программе
|
|
||||||
OR bl.project_id IN (SELECT id FROM v3.form4_project pr -- через проект
|
|
||||||
WHERE pr.parent_id = ap.prog_id AND pr.level = 'project')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- ITEM — из агрегатов (проекты с бюджетными строками)
|
|
||||||
SELECT
|
|
||||||
'ITEM', 2, NULL,
|
|
||||||
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
|
||||||
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
|
||||||
plan_data, seq_dfip_data, approved_data,
|
|
||||||
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
|
||||||
contract_data, booking_data,
|
|
||||||
q1_data, q2_data, q3_data, q4_data, totals_data,
|
|
||||||
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
|
||||||
FROM prj_agg
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- ITEM — пустые проекты (нет budget_line)
|
|
||||||
SELECT
|
|
||||||
'ITEM', 2, NULL,
|
|
||||||
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
|
||||||
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
|
||||||
tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved
|
|
||||||
tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve
|
|
||||||
tp_collegial, tp_ckk, -- collegial, ckk
|
|
||||||
tp_contract, tp_booking, -- contract, booking
|
|
||||||
tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals
|
|
||||||
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
|
||||||
FROM all_projects apr
|
|
||||||
JOIN tp_agg ON tp_agg.tp_id = 1
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM v3.budget_line bl
|
|
||||||
WHERE bl.budget_form_id = p_form_id
|
|
||||||
AND bl.project_id = apr.prj_id
|
|
||||||
)
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- INPUT — все строки бюджета
|
|
||||||
SELECT
|
|
||||||
'INPUT', 3, line_id,
|
|
||||||
header || jsonb_build_object(
|
|
||||||
'project_id', prj_id, 'project_name', prj_name,
|
|
||||||
'program_id', prog_id, 'program_name', prog_name
|
|
||||||
),
|
|
||||||
plan_data, seq_dfip_data, approved_data,
|
|
||||||
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
|
||||||
contract_data, booking_data,
|
|
||||||
q1_data, q2_data, q3_data, q4_data, totals_data,
|
|
||||||
ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(CASE WHEN prj_id is null and prog_id is not null THEN 0 ELSE prj_id END, 999999), line_id]::INT[]
|
|
||||||
FROM inp
|
|
||||||
)
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
u.row_type, u.depth,
|
|
||||||
ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order,
|
|
||||||
jsonb_build_object(
|
|
||||||
'sort_path', u.sort_path,
|
|
||||||
'line_id', u.line_id,
|
|
||||||
'header', u.header,
|
|
||||||
'plan', u.plan_data,
|
|
||||||
'seq_dfip', u.seq_dfip_data,
|
|
||||||
'approved', u.approved_data,
|
|
||||||
'contract_summary', u.contract_summary_data,
|
|
||||||
'allocation', u.allocation_data,
|
|
||||||
'reserve', u.reserve_data,
|
|
||||||
'collegial', u.collegial_data,
|
|
||||||
'ckk', u.ckk_data,
|
|
||||||
'contract', u.contract_data,
|
|
||||||
'booking', u.booking_data,
|
|
||||||
'q1', u.q1_data,
|
|
||||||
'q2', u.q2_data,
|
|
||||||
'q3', u.q3_data,
|
|
||||||
'q4', u.q4_data,
|
|
||||||
'totals', u.totals_data
|
|
||||||
) AS data
|
|
||||||
FROM unioned u
|
|
||||||
ORDER BY u.sort_path;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user