-- ════════════════════════════════════════════════════════════════════════════ -- МИГРАЦИЯ: Поля project + реструктуризация rf_project_report -- (year -> rf_project_year, атрибуты сметы -> smeta) -- ════════════════════════════════════════════════════════════════════════════ -- Поля project ALTER TABLE v3.project ADD COLUMN status VARCHAR DEFAULT 'created'; ALTER TABLE v3.project ADD COLUMN technical_number VARCHAR; ALTER TABLE v3.project ADD COLUMN krf_decision_date DATE; ALTER TABLE v3.project ADD COLUMN fk_decision_date DATE; ALTER TABLE v3.project ADD COLUMN board_decision_date DATE; ALTER TABLE v3.project ADD COLUMN open_relocate_close_date DATE; ALTER TABLE v3.project ADD COLUMN funding_by_ko_decision VARCHAR; ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_status_check; ALTER TABLE v3.project ADD CONSTRAINT project_status_check CHECK (status IS NULL OR status IN ('created', 'agreed', 'archived')); ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_funding_ko_check; ALTER TABLE v3.project ADD CONSTRAINT project_funding_ko_check CHECK (funding_by_ko_decision IS NULL OR funding_by_ko_decision IN ('prrs_budget', 'bank_reserve')); -- rf_project_year: год проекта как отдельная сущность CREATE TABLE v3.rf_project_year ( id SERIAL PRIMARY KEY, project_id INTEGER NOT NULL REFERENCES v3.project(id), year INTEGER NOT NULL, CONSTRAINT uq_v3_rf_project_year UNIQUE (project_id, year) ); CREATE INDEX ix_v3_rf_project_year_project ON v3.rf_project_year(project_id); -- Перенос существующих year из rf_project_report в rf_project_year INSERT INTO v3.rf_project_year (project_id, year) SELECT DISTINCT project_id, year FROM v3.rf_project_report; -- Связь rf_project_report с rf_project_year ALTER TABLE v3.rf_project_report ADD COLUMN rf_project_year_id INTEGER; UPDATE v3.rf_project_report rpr SET rf_project_year_id = rpy.id FROM v3.rf_project_year rpy WHERE rpy.project_id = rpr.project_id AND rpy.year = rpr.year; ALTER TABLE v3.rf_project_report ALTER COLUMN rf_project_year_id SET NOT NULL; ALTER TABLE v3.rf_project_report ADD CONSTRAINT fk_v3_rf_project_report_year FOREIGN KEY (rf_project_year_id) REFERENCES v3.rf_project_year(id); -- Убираем year из rf_project_report ALTER TABLE v3.rf_project_report DROP CONSTRAINT IF EXISTS uq_v3_rf_project_report; DROP INDEX IF EXISTS v3.ix_v3_rf_project_report_proj_year; ALTER TABLE v3.rf_project_report DROP COLUMN year; -- Восстанавливаем уникальность и индекс на новой связке ALTER TABLE v3.rf_project_report ADD CONSTRAINT uq_v3_rf_project_report UNIQUE (project_id, rf_project_year_id, report_type); CREATE INDEX ix_v3_rf_project_report_proj_year ON v3.rf_project_report(project_id, rf_project_year_id); -- smeta: атрибуты сметы на год проекта CREATE TABLE v3.smeta ( id SERIAL PRIMARY KEY, rf_project_year_id INTEGER NOT NULL REFERENCES v3.rf_project_year(id), is_in_plan BOOLEAN, is_in_plan_q2 BOOLEAN, is_in_plan_q3 BOOLEAN, is_in_plan_q4 BOOLEAN, development_block VARCHAR, reserve_to_prrs_ahr NUMERIC, reserve_to_prrs_kv NUMERIC, CONSTRAINT smeta_development_block_check CHECK (development_block IS NULL OR development_block IN ('out_of_base_plan', 'carryover', 'additional_funding')) ); -- Funcs CREATE OR REPLACE FUNCTION v3.add_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone) RETURNS v3.form3_phase LANGUAGE plpgsql AS $function$ DECLARE v_phase v3.form3_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_utc_offset VARCHAR(3); v_report_type VARCHAR; v_project_id INT; BEGIN SELECT p.org_unit_id, ou.title, rpry.year, ou.utc_offset, rpr.report_type, rpr.project_id INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id FROM v3.rf_project_report rpr JOIN v3.rf_project_year rpry ON rpr.rf_project_year_id = rpry.id LEFT JOIN v3.project p ON p.id = rpr.project_id LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id WHERE rpr.id = p_rf_project_report_id; INSERT INTO v3.form3_phase (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at) VALUES ( p_rf_project_report_id, p_phase_code, p_role, p_column_keys, CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END, CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END ) RETURNING * INTO v_phase; v_task_name := trim(concat( 'FORM_3', CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'create', 'phase_id', v_phase_id, 'task_name', NULLIF(v_task_name, ''), 'report_id', p_rf_project_report_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'year', v_year, 'project_id', v_project_id, 'report_type', v_report_type, 'phase_code', p_phase_code, 'role', v_phase.role, 'column_keys', v_phase.column_keys, 'opens_at', v_phase.opens_at, 'closes_at', v_phase.closes_at ), null, null, v_org_unit_id ); RETURN v_phase; END; $function$ ; -- DROP FUNCTION v3.add_project_year(int4, int4, bool); CREATE OR REPLACE FUNCTION v3.add_project_year(p_project_id integer, p_year integer, p_emit_audit boolean DEFAULT true) RETURNS TABLE(limit_report_id integer, current_expenses_report_id integer) LANGUAGE plpgsql AS $function$ DECLARE v_ryid INT; 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_year 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_year (project_id, year) VALUES (p_project_id, p_year) RETURNING id INTO v_ryid; INSERT INTO v3.smeta (rf_project_year_id) VALUES (v_ryid); INSERT INTO v3.rf_project_report (project_id, rf_project_year_id, report_type) VALUES (p_project_id, v_ryid, 'LIMIT') RETURNING id INTO v_lim; INSERT INTO v3.rf_project_report (project_id, rf_project_year_id, report_type) VALUES (p_project_id, v_ryid, 'CURRENT_EXPENSES') RETURNING id INTO v_cur; 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', 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$ ; -- DROP FUNCTION v3.del_form3_phase(int4, varchar); CREATE OR REPLACE FUNCTION v3.del_form3_phase(p_rf_project_report_id integer, p_phase_code character varying) RETURNS boolean LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form3_phase; v_org_unit_id INT; v_org_unit_title VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_report_type VARCHAR; v_project_id INT; BEGIN DELETE FROM v3.form3_phase WHERE rf_project_report_id = p_rf_project_report_id AND phase_code = p_phase_code RETURNING * INTO v_old; IF NOT FOUND THEN RETURN FALSE; END IF; SELECT p.org_unit_id, ou.title, rpry.year, rpr.report_type, rpr.project_id INTO v_org_unit_id, v_org_unit_title, v_year, v_report_type, v_project_id FROM v3.rf_project_report rpr JOIN v3.rf_project_year rpry ON rpr.rf_project_year_id = rpry.id LEFT JOIN v3.project p ON p.id = rpr.project_id LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id WHERE rpr.id = p_rf_project_report_id; v_task_name := trim(concat( 'FORM_3', CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code); PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'delete', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_name', NULLIF(v_task_name, ''), 'project_id', v_project_id, 'year', v_year, 'report_type', v_report_type, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'phase_code', p_phase_code, 'role', v_old.role, 'column_keys', v_old.column_keys, 'opens_at', v_old.opens_at, 'closes_at', v_old.closes_at ), null, null, v_org_unit_id ); RETURN TRUE; END; $function$ ; -- DROP FUNCTION v3.upd_form3_phase(int4, varchar, varchar, _text, timestamp, timestamp); CREATE OR REPLACE FUNCTION v3.upd_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS v3.form3_phase LANGUAGE plpgsql AS $function$ DECLARE v_old v3.form3_phase; v_new v3.form3_phase; v_is_extend BOOLEAN := FALSE; v_has_non_extend_change BOOLEAN := FALSE; v_has_window_change BOOLEAN := FALSE; v_changes JSONB := '{}'::jsonb; v_org_unit_id INT; v_org_unit_title VARCHAR; v_form_type_code VARCHAR; v_year INT; v_task_name TEXT; v_phase_id TEXT; v_utc_offset VARCHAR(3); v_opens_at timestamptz; v_closes_at timestamptz; v_report_type VARCHAR; v_project_id INT; BEGIN SELECT * INTO v_old FROM v3.form3_phase WHERE rf_project_report_id = p_rf_project_report_id AND phase_code = p_phase_code; IF NOT FOUND THEN RAISE EXCEPTION 'form3_phase не найден (rf_project_report_id=%, phase=%)', p_rf_project_report_id, p_phase_code; END IF; SELECT p.org_unit_id, ou.title, rpry.year, ou.utc_offset, rpr.report_type, rpr.project_id INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id FROM v3.rf_project_report rpr JOIN v3.rf_project_year rpry ON rpr.rf_project_year_id = rpry.id LEFT JOIN v3.project p ON p.id = rpr.project_id LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id WHERE rpr.id = p_rf_project_report_id; v_task_name := trim(concat( 'FORM_3', CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END, CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END )); IF p_opens_at is not null THEN v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz; END IF; IF p_closes_at is not null THEN v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz; END IF; UPDATE v3.form3_phase SET role = COALESCE(p_role, role), column_keys = COALESCE(p_column_keys, column_keys), opens_at = COALESCE(v_opens_at, opens_at), closes_at = COALESCE(v_closes_at, closes_at) WHERE rf_project_report_id = p_rf_project_report_id AND phase_code = p_phase_code RETURNING * INTO v_new; v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code); v_is_extend := v_new.closes_at > v_old.closes_at; v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role OR v_old.column_keys IS DISTINCT FROM v_new.column_keys OR v_old.opens_at IS DISTINCT FROM v_new.opens_at; v_has_window_change := v_has_non_extend_change OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend); IF v_old.role IS DISTINCT FROM v_new.role THEN v_changes := v_changes || jsonb_build_object( 'role', jsonb_build_object('before', v_old.role, 'after', v_new.role) ); END IF; IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN v_changes := v_changes || jsonb_build_object( 'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys) ); END IF; IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN v_changes := v_changes || jsonb_build_object( 'opens_at', jsonb_build_object( 'before', v_old.opens_at, 'after', v_new.opens_at ) ); END IF; IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN v_changes := v_changes || jsonb_build_object( 'closes_at', jsonb_build_object( 'before', v_old.closes_at, 'after', v_new.closes_at ) ); END IF; IF v_is_extend THEN PERFORM v3.log_event( 'ACCESS_EXTEND', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'update', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_name', NULLIF(v_task_name, ''), 'report_id', p_rf_project_report_id, 'report_type', v_report_type, 'project_id', v_project_id, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'year', v_year, 'phase_code', p_phase_code, 'closes_at_before', v_old.closes_at, 'closes_at_after', v_new.closes_at ), null, null, v_org_unit_id ); END IF; IF v_has_window_change THEN PERFORM v3.log_event( 'ACCESS_WINDOW_CHANGE', 'ACCESS', jsonb_build_object( 'entity_type', 'project', 'entity_id', v_project_id, 'core_entity_type', 'form3_phase', 'core_entity_id', v_phase_id, 'action', 'update', 'phase_id', v_phase_id, 'phase_name', p_phase_code, 'task_id', p_rf_project_report_id, 'task_name', NULLIF(v_task_name, ''), 'project_id', v_project_id, 'report_type', v_report_type, 'year', v_year, 'org_unit_id', v_org_unit_id, 'org_unit_name', v_org_unit_title, 'phase_code', p_phase_code, 'changes', v_changes ), null, null, v_org_unit_id ); END IF; RETURN v_new; END; $function$ ; -- DROP FUNCTION v3.v_form3_project_jsonb(int4, varchar, _text); CREATE OR REPLACE FUNCTION v3.v_form3_project_jsonb(p_project_id integer, p_report_type character varying, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql STABLE AS $function$ WITH reports AS ( SELECT r.id AS report_id, rpry.year FROM v3.rf_project_report r JOIN v3.rf_project_year rpry ON rpry.id = r.rf_project_year_id 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$ ; -- DROP FUNCTION v3.v_form3_rf_rollup_sections(int4, int4, _text); CREATE OR REPLACE FUNCTION v3.v_form3_rf_rollup_sections(p_branch_id integer, p_year integer, p_sections text[] DEFAULT NULL::text[]) RETURNS TABLE(row_type character varying, depth integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_q1_adj_by_items numeric, col_q1_adj_increase numeric, col_q1_total_corr numeric, col_q1_m1 numeric, col_q1_m2 numeric, col_q1_m3 numeric, col_q1_quarter_actual numeric, col_q1_economy numeric, col_q2_adj_by_items numeric, col_q2_adj_increase numeric, col_q2_total_corr numeric, col_q2_m1 numeric, col_q2_m2 numeric, col_q2_m3 numeric, col_q2_quarter_actual numeric, col_q2_economy numeric, col_q3_adj_by_items numeric, col_q3_adj_increase numeric, col_q3_total_corr numeric, col_q3_m1 numeric, col_q3_m2 numeric, col_q3_m3 numeric, col_q3_quarter_actual numeric, col_q3_economy numeric, col_q4_adj_by_items numeric, col_q4_adj_increase numeric, col_q4_total_corr numeric, col_q4_m1 numeric, col_q4_m2 numeric, col_q4_m3 numeric, col_q4_spod numeric, col_q4_quarter_actual numeric, col_q4_economy numeric, col_year_total_corr numeric, col_year_total_actual numeric, col_year_economy numeric, _sort_path integer[]) LANGUAGE plpgsql STABLE AS $function$ DECLARE s_q1 BOOLEAN := p_sections IS NULL OR 'q1' = ANY(p_sections); s_q2 BOOLEAN := p_sections IS NULL OR 'q2' = ANY(p_sections); s_q3 BOOLEAN := p_sections IS NULL OR 'q3' = ANY(p_sections); s_q4 BOOLEAN := p_sections IS NULL OR 'q4' = ANY(p_sections); s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections); BEGIN RETURN QUERY WITH tw AS ( SELECT t.* FROM v3.mv_expense_item_tree t JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3' ), -- Агрегаты per (expense_item_id, quarter) — sum по обоим трекам всех проектов РФ × год agg_q AS ( SELECT l.expense_item_id AS eid, q.quarter, SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai, SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, SUM(q.actual_spod) AS spod FROM v3.rf_project_report r JOIN v3.rf_project_year rpry ON rpry.id = r.rf_project_year_id JOIN v3.project p ON p.id = r.project_id JOIN v3.rf_project_report_line l ON l.rf_project_report_id = r.id JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id WHERE p.org_unit_id = p_branch_id AND rpry.year = p_year GROUP BY l.expense_item_id, q.quarter ), tw_aq1 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=1 GROUP BY tw.id), tw_aq2 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=2 GROUP BY tw.id), tw_aq3 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3 FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=3 GROUP BY tw.id), tw_aq4 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai, SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3, SUM(a.spod) AS spod FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=4 GROUP BY tw.id) SELECT CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR, t.depth, CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END, CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END, t.num_group_id, t.name, -- Q1 CASE WHEN s_q1 THEN a1.abi END, CASE WHEN s_q1 THEN a1.ai END, CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END, CASE WHEN s_q1 THEN a1.m1 END, CASE WHEN s_q1 THEN a1.m2 END, CASE WHEN s_q1 THEN a1.m3 END, CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END, CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0)) -(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END, -- Q2 CASE WHEN s_q2 THEN a2.abi END, CASE WHEN s_q2 THEN a2.ai END, CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END, CASE WHEN s_q2 THEN a2.m1 END, CASE WHEN s_q2 THEN a2.m2 END, CASE WHEN s_q2 THEN a2.m3 END, CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END, CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0)) -(COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END, -- Q3 CASE WHEN s_q3 THEN a3.abi END, CASE WHEN s_q3 THEN a3.ai END, CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END, CASE WHEN s_q3 THEN a3.m1 END, CASE WHEN s_q3 THEN a3.m2 END, CASE WHEN s_q3 THEN a3.m3 END, CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END, CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0)) -(COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END, -- Q4 CASE WHEN s_q4 THEN a4.abi END, CASE WHEN s_q4 THEN a4.ai END, CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END, CASE WHEN s_q4 THEN a4.m1 END, CASE WHEN s_q4 THEN a4.m2 END, CASE WHEN s_q4 THEN a4.m3 END, CASE WHEN s_q4 THEN a4.spod END, CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END, CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) -(COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END, -- year CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+ COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END, CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+ COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+ COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+ COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END, CASE WHEN s_year THEN ROUND( (COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+ COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) -(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+ COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+ COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+ COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END, t.path FROM tw t LEFT JOIN tw_aq1 a1 ON a1.id = t.id LEFT JOIN tw_aq2 a2 ON a2.id = t.id LEFT JOIN tw_aq3 a3 ON a3.id = t.id LEFT JOIN tw_aq4 a4 ON a4.id = t.id ORDER BY t.path; END; $function$ ; CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_report_type VARCHAR; v_inserted INTEGER; v_org_unit INTEGER; v_utc_offset VARCHAR(3); v_year INTEGER; BEGIN SELECT report_type, org_unit_id, "year" INTO v_report_type, v_org_unit, v_year FROM v3.rf_project_report AS rpr INNER JOIN v3.project AS p ON p.id = rpr.project_id INNER JOIN v3.rf_project_year AS rpy ON rpy.id = rpr.rf_project_year_id WHERE rpr.id = p_report_id; IF NOT FOUND THEN RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; END IF; SELECT utc_offset INTO v_utc_offset FROM v3.org_unit WHERE id = v_org_unit; INSERT INTO v3.form3_phase (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at) SELECT p_report_id, pt.phase_code, pt.role, pt.column_keys, ((pt.opens_at + INTERVAL '1 year' * v_year)::text || v_utc_offset)::timestamptz, ((pt.closes_at + INTERVAL '1 year' * v_year)::text || v_utc_offset)::timestamptz FROM v3.phase_template pt WHERE pt.form_type = 'FORM_3' AND pt.sheet = v_report_type ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING; GET DIAGNOSTICS v_inserted = ROW_COUNT; RETURN v_inserted; END; $function$ ; -- DROP FUNCTION v3.del_project(int4); CREATE OR REPLACE FUNCTION v3.del_project(p_project_id integer) RETURNS integer LANGUAGE plpgsql AS $function$ DECLARE v_old RECORD; v_actor_id INTEGER; v_role VARCHAR; BEGIN SELECT p.name, p.level, p.org_unit_id INTO v_old FROM v3.project p WHERE p.id = p_project_id; IF NOT FOUND THEN RAISE EXCEPTION 'project #% не существует', p_project_id; END IF; IF NOT v3.can_delete_project(p_project_id) THEN RAISE EXCEPTION 'project #% невозможно удалить', p_project_id; END IF; DELETE FROM v3.form3_phase f3p WHERE f3p.rf_project_report_id IN ( SELECT id FROM v3.rf_project_report rpr WHERE project_id = p_project_id ); DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id IN ( SELECT l.id FROM v3.rf_project_report_line l JOIN v3.rf_project_report r ON r.id = l.rf_project_report_id WHERE r.project_id = p_project_id); DELETE FROM v3.rf_project_report_line WHERE rf_project_report_id IN ( SELECT id FROM v3.rf_project_report WHERE project_id = p_project_id); DELETE FROM v3.rf_project_report WHERE project_id = p_project_id; DELETE FROM v3.smeta WHERE rf_project_year_id IN ( SELECT id FROM v3.rf_project_year WHERE project_id = p_project_id ); DELETE FROM v3.rf_project_year WHERE project_id = p_project_id; DELETE FROM v3.project WHERE id = p_project_id; PERFORM v3.log_event( 'PROJECT_DELETE', 'PROJECT', jsonb_build_object( 'project_id', p_project_id, 'name', v_old.name, 'level', v_old.level, 'org_unit_id', v_old.org_unit_id), null, null, v_old.org_unit_id); RETURN p_project_id; END; $function$ ; -- DROP FUNCTION v3.upd_project(int4, varchar, varchar, varchar, varchar, varchar, varchar, varchar, int4, numeric, int4, date, date, date, date, varchar); CREATE OR REPLACE FUNCTION v3.upd_project( p_project_id integer, p_name character varying DEFAULT NULL, p_status character varying DEFAULT NULL, p_technical_number character varying DEFAULT NULL, p_project_type character varying DEFAULT NULL, p_vsp_format character varying DEFAULT NULL, p_placement_type character varying DEFAULT NULL, p_object_address character varying DEFAULT NULL, p_staff_count integer DEFAULT NULL, p_total_area numeric DEFAULT NULL, p_org_unit_id integer DEFAULT NULL, p_krf_decision_date date DEFAULT NULL, p_fk_decision_date date DEFAULT NULL, p_board_decision_date date DEFAULT NULL, p_open_relocate_close_date date DEFAULT NULL, p_funding_by_ko_decision character varying DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE v_old v3.project%ROWTYPE; v_changes JSONB := '{}'::jsonb; BEGIN SELECT * INTO v_old FROM v3.project WHERE id = p_project_id; IF NOT FOUND THEN RAISE EXCEPTION 'project id=% не существует', p_project_id; END IF; IF p_name IS NOT NULL AND p_name IS DISTINCT FROM v_old.name THEN UPDATE v3.project SET name = p_name WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('name', jsonb_build_object('before', v3._diff_val(v_old.name), 'after', v3._diff_val(p_name))); END IF; IF p_status IS NOT NULL AND p_status IS DISTINCT FROM v_old.status THEN UPDATE v3.project SET status = p_status WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('before', v3._diff_val(v_old.status), 'after', v3._diff_val(p_status))); END IF; IF p_technical_number IS NOT NULL AND p_technical_number IS DISTINCT FROM v_old.technical_number THEN UPDATE v3.project SET technical_number = p_technical_number WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('technical_number', jsonb_build_object('before', v3._diff_val(v_old.technical_number), 'after', v3._diff_val(p_technical_number))); END IF; IF p_project_type IS NOT NULL AND p_project_type IS DISTINCT FROM v_old.project_type THEN UPDATE v3.project SET project_type = p_project_type WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('project_type', jsonb_build_object('before', v3._diff_val(v_old.project_type), 'after', v3._diff_val(p_project_type))); END IF; IF p_vsp_format IS NOT NULL AND p_vsp_format IS DISTINCT FROM v_old.vsp_format THEN UPDATE v3.project SET vsp_format = p_vsp_format WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('vsp_format', jsonb_build_object('before', v3._diff_val(v_old.vsp_format), 'after', v3._diff_val(p_vsp_format))); END IF; IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN UPDATE v3.project SET placement_type = p_placement_type WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('placement_type', jsonb_build_object('before', v3._diff_val(v_old.placement_type), 'after', v3._diff_val(p_placement_type))); END IF; IF p_object_address IS NOT NULL AND p_object_address IS DISTINCT FROM v_old.object_address THEN UPDATE v3.project SET object_address = p_object_address WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('object_address', jsonb_build_object('before', v3._diff_val(v_old.object_address), 'after', v3._diff_val(p_object_address))); END IF; IF p_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN UPDATE v3.project SET staff_count = p_staff_count WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('staff_count', jsonb_build_object('before', v3._diff_val(v_old.staff_count), 'after', v3._diff_val(p_staff_count))); END IF; IF p_total_area IS NOT NULL AND p_total_area IS DISTINCT FROM v_old.total_area THEN UPDATE v3.project SET total_area = p_total_area WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('total_area', jsonb_build_object('before', v3._diff_val(v_old.total_area), 'after', v3._diff_val(p_total_area))); END IF; IF p_org_unit_id IS NOT NULL AND p_org_unit_id IS DISTINCT FROM v_old.org_unit_id THEN 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; UPDATE v3.project SET org_unit_id = p_org_unit_id WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('org_unit_id', jsonb_build_object('before', v3._diff_val(v_old.org_unit_id), 'after', v3._diff_val(p_org_unit_id))); END IF; IF p_krf_decision_date IS NOT NULL AND p_krf_decision_date IS DISTINCT FROM v_old.krf_decision_date THEN UPDATE v3.project SET krf_decision_date = p_krf_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('krf_decision_date', jsonb_build_object('before', v3._diff_val(v_old.krf_decision_date), 'after', v3._diff_val(p_krf_decision_date))); END IF; IF p_fk_decision_date IS NOT NULL AND p_fk_decision_date IS DISTINCT FROM v_old.fk_decision_date THEN UPDATE v3.project SET fk_decision_date = p_fk_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('fk_decision_date', jsonb_build_object('before', v3._diff_val(v_old.fk_decision_date), 'after', v3._diff_val(p_fk_decision_date))); END IF; IF p_board_decision_date IS NOT NULL AND p_board_decision_date IS DISTINCT FROM v_old.board_decision_date THEN UPDATE v3.project SET board_decision_date = p_board_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('board_decision_date', jsonb_build_object('before', v3._diff_val(v_old.board_decision_date), 'after', v3._diff_val(p_board_decision_date))); END IF; IF p_open_relocate_close_date IS NOT NULL AND p_open_relocate_close_date IS DISTINCT FROM v_old.open_relocate_close_date THEN UPDATE v3.project SET open_relocate_close_date = p_open_relocate_close_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('open_relocate_close_date', jsonb_build_object('before', v3._diff_val(v_old.open_relocate_close_date), 'after', v3._diff_val(p_open_relocate_close_date))); END IF; IF p_funding_by_ko_decision IS NOT NULL AND p_funding_by_ko_decision IS DISTINCT FROM v_old.funding_by_ko_decision THEN UPDATE v3.project SET funding_by_ko_decision = p_funding_by_ko_decision WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('funding_by_ko_decision', jsonb_build_object('before', v3._diff_val(v_old.funding_by_ko_decision), 'after', v3._diff_val(p_funding_by_ko_decision))); END IF; IF v_changes <> '{}'::jsonb THEN PERFORM v3.log_event( 'PROJECT_UPDATE', 'PROJECT', jsonb_build_object( 'project_id', p_project_id, 'entity_id', p_project_id, 'changes', v_changes ), null, null, COALESCE(p_org_unit_id, v_old.org_unit_id) ); END IF; END; $function$ ; -- DROP FUNCTION v3.upd_smeta(int4, bool, bool, bool, bool, varchar, numeric, numeric); CREATE OR REPLACE FUNCTION v3.upd_smeta( p_rf_project_year_id integer, p_is_in_plan boolean DEFAULT NULL, p_is_in_plan_q2 boolean DEFAULT NULL, p_is_in_plan_q3 boolean DEFAULT NULL, p_is_in_plan_q4 boolean DEFAULT NULL, p_development_block character varying DEFAULT NULL, p_reserve_to_prrs_ahr numeric DEFAULT NULL, p_reserve_to_prrs_kv numeric DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE v_old v3.smeta%ROWTYPE; v_changes JSONB := '{}'::jsonb; v_project_id INT; v_org_unit_id INT; BEGIN IF NOT EXISTS (SELECT 1 FROM v3.rf_project_year WHERE id = p_rf_project_year_id) THEN RAISE EXCEPTION 'rf_project_year id=% не существует', p_rf_project_year_id; END IF; INSERT INTO v3.smeta (rf_project_year_id) SELECT p_rf_project_year_id WHERE NOT EXISTS (SELECT 1 FROM v3.smeta WHERE rf_project_year_id = p_rf_project_year_id); SELECT * INTO v_old FROM v3.smeta WHERE rf_project_year_id = p_rf_project_year_id; IF p_is_in_plan IS NOT NULL AND p_is_in_plan IS DISTINCT FROM v_old.is_in_plan THEN UPDATE v3.smeta SET is_in_plan = p_is_in_plan WHERE id = v_old.id; v_changes := v_changes || jsonb_build_object('is_in_plan', jsonb_build_object('before', v3._diff_val(v_old.is_in_plan), 'after', v3._diff_val(p_is_in_plan))); END IF; IF p_is_in_plan_q2 IS NOT NULL AND p_is_in_plan_q2 IS DISTINCT FROM v_old.is_in_plan_q2 THEN UPDATE v3.smeta SET is_in_plan_q2 = p_is_in_plan_q2 WHERE id = v_old.id; v_changes := v_changes || jsonb_build_object('is_in_plan_q2', jsonb_build_object('before', v3._diff_val(v_old.is_in_plan_q2), 'after', v3._diff_val(p_is_in_plan_q2))); END IF; IF p_is_in_plan_q3 IS NOT NULL AND p_is_in_plan_q3 IS DISTINCT FROM v_old.is_in_plan_q3 THEN UPDATE v3.smeta SET is_in_plan_q3 = p_is_in_plan_q3 WHERE id = v_old.id; v_changes := v_changes || jsonb_build_object('is_in_plan_q3', jsonb_build_object('before', v3._diff_val(v_old.is_in_plan_q3), 'after', v3._diff_val(p_is_in_plan_q3))); END IF; IF p_is_in_plan_q4 IS NOT NULL AND p_is_in_plan_q4 IS DISTINCT FROM v_old.is_in_plan_q4 THEN UPDATE v3.smeta SET is_in_plan_q4 = p_is_in_plan_q4 WHERE id = v_old.id; v_changes := v_changes || jsonb_build_object('is_in_plan_q4', jsonb_build_object('before', v3._diff_val(v_old.is_in_plan_q4), 'after', v3._diff_val(p_is_in_plan_q4))); END IF; IF p_development_block IS NOT NULL AND p_development_block IS DISTINCT FROM v_old.development_block THEN UPDATE v3.smeta SET development_block = p_development_block WHERE id = v_old.id; v_changes := v_changes || jsonb_build_object('development_block', jsonb_build_object('before', v3._diff_val(v_old.development_block), 'after', v3._diff_val(p_development_block))); END IF; IF p_reserve_to_prrs_ahr IS NOT NULL AND p_reserve_to_prrs_ahr IS DISTINCT FROM v_old.reserve_to_prrs_ahr THEN UPDATE v3.smeta SET reserve_to_prrs_ahr = p_reserve_to_prrs_ahr WHERE id = v_old.id; v_changes := v_changes || jsonb_build_object('reserve_to_prrs_ahr', jsonb_build_object('before', v3._diff_val(v_old.reserve_to_prrs_ahr), 'after', v3._diff_val(p_reserve_to_prrs_ahr))); END IF; IF p_reserve_to_prrs_kv IS NOT NULL AND p_reserve_to_prrs_kv IS DISTINCT FROM v_old.reserve_to_prrs_kv THEN UPDATE v3.smeta SET reserve_to_prrs_kv = p_reserve_to_prrs_kv WHERE id = v_old.id; v_changes := v_changes || jsonb_build_object('reserve_to_prrs_kv', jsonb_build_object('before', v3._diff_val(v_old.reserve_to_prrs_kv), 'after', v3._diff_val(p_reserve_to_prrs_kv))); END IF; SELECT rpy.project_id, p.org_unit_id INTO v_project_id, v_org_unit_id FROM v3.rf_project_year rpy JOIN v3.project p ON p.id = rpy.project_id WHERE rpy.id = p_rf_project_year_id; IF v_changes <> '{}'::jsonb THEN PERFORM v3.log_event( 'SMETA_UPDATE', 'PROJECT', jsonb_build_object( 'rf_project_year_id', p_rf_project_year_id, 'project_id', v_project_id, 'changes', v_changes ), null, null, v_org_unit_id ); END IF; END; $function$ ;