From 9bf17c6eb394a26c3128a318a0cf054a6d302b39 Mon Sep 17 00:00:00 2001 From: tsygankoviva Date: Thu, 30 Jul 2026 14:25:03 +0300 Subject: [PATCH] =?UTF-8?q?delete-projects:=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D0=B2,=20=D1=84=D0=B8=D0=BA=D1=81=20=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=B2=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B5=204,=20=D0=BD=D0=B5=D0=BB=D1=8C=D0=B7=D1=8F=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20=D1=81=D0=B0=D0=BC?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=81=D0=B5=D0=B1=D1=8F,=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D1=8F=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BD=D0=B5=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=81=D1=81=D0=BF=20=D0=B4=D0=BB=D1=8F=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20=D0=B2=D1=81=D0=BF,=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=D0=B0=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20(=D0=B1=D1=8D=D0=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/alembic/versions/0012_del_project.py | 84 + api/alembic/versions/sql/0012_del_project.sql | 2464 +++++++++++++++++ api/src/api/v1/projects.py | 12 + api/src/api/v1/websocket.py | 4 +- api/src/db/models/project.py | 5 +- api/src/repository/project_repository.py | 10 + api/src/services/project_service.py | 6 + api/src/services/user_service.py | 9 + .../integration/test_projects_api_smoke.py | 113 +- .../RealtimeTable/constants/FORM_4/AHR.js | 102 +- web/src/pages/DictVspPage/DictVspPage.jsx | 2 +- 11 files changed, 2752 insertions(+), 59 deletions(-) create mode 100644 api/alembic/versions/0012_del_project.py create mode 100644 api/alembic/versions/sql/0012_del_project.sql diff --git a/api/alembic/versions/0012_del_project.py b/api/alembic/versions/0012_del_project.py new file mode 100644 index 0000000..378f0ad --- /dev/null +++ b/api/alembic/versions/0012_del_project.py @@ -0,0 +1,84 @@ +import os +import re +import time + +from alembic import op +import sqlalchemy as sa + + +revision = "0012" +down_revision = "0011" +branch_labels = None +depends_on = None + + +_DOLLAR_TAG_RE = re.compile(r"\$\w+\$") + + +def _find_dollar_tag(line: str) -> str | None: + m = _DOLLAR_TAG_RE.search(line.strip()) + return m.group(0) if m else None + + +def _split_statements(sql: str) -> list[str]: + statements: list[str] = [] + current: list[str] = [] + in_dollar = False + dollar_tag: str | None = None + + for line in sql.split("\n"): + stripped = line.strip() + if stripped.startswith("--"): + continue + + if not in_dollar: + tag = _find_dollar_tag(stripped) + if tag and tag.endswith("$") and tag.startswith("$"): + dollar_tag = tag + in_dollar = True + current.append(line) + continue + + if in_dollar and dollar_tag and stripped.startswith(dollar_tag): + after = stripped[len(dollar_tag):].strip() + if after == ";" or after == "": + in_dollar = False + dollar_tag = None + if after == ";": + current.append(line) + statements.append("\n".join(current)) + current = [] + continue + + if not in_dollar and stripped.rstrip().endswith(";"): + current.append(line) + statements.append("\n".join(current)) + current = [] + continue + + current.append(line) + + remaining = "\n".join(current).strip() + if remaining: + statements.append(remaining) + + return statements + + +def upgrade() -> None: + ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0012_del_project.sql") + with open(ddl_path) as f: + content = f.read() + + statements = _split_statements(content) + for stmt in statements: + stripped = stmt.strip().rstrip(";").strip() + if not stripped: + continue + if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")): + continue + op.execute(stripped) + + +def downgrade() -> None: + pass diff --git a/api/alembic/versions/sql/0012_del_project.sql b/api/alembic/versions/sql/0012_del_project.sql new file mode 100644 index 0000000..55e5668 --- /dev/null +++ b/api/alembic/versions/sql/0012_del_project.sql @@ -0,0 +1,2464 @@ +-- удаление проектов +CREATE OR REPLACE FUNCTION v3.can_delete_project(p_project_id integer) +RETURNS BOOL +LANGUAGE plpgsql +AS $function$ +DECLARE + v_old RECORD; + v_actor_id INTEGER; + v_role VARCHAR; + -- v_phase v3.form3_phase; +BEGIN + v_actor_id := v3.current_user_id(); + v_role := v3.user_role_code(v_actor_id); + + SELECT p.name, p.level, p.org_unit_id, p.created_at + INTO v_old + FROM v3.project p WHERE p.id = p_project_id; + + IF v_role != 'ADMIN' AND NOT EXISTS ( + SELECT 1 FROM v3.user_org uo + WHERE uo.org_unit_id = v_old.org_unit_id AND uo.user_id = v_actor_id + ) THEN + RETURN false; + END IF; + + IF ( + select count(*) from ( + select rpr.id, max(fp.closes_at) from v3.form3_phase fp + inner join v3.rf_project_report rpr + on rpr.id = fp.rf_project_report_id + and rpr.project_id = p_project_id + and fp.opens_at <= current_timestamp + and current_timestamp <= fp.closes_at + and fp.opens_at <= v_old.created_at + and v_old.created_at <= fp.closes_at + group by rpr.id + ) + ) = (select COUNT(*) from v3.rf_project_report rpr where rpr.project_id = p_project_id) THEN + RETURN true; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM v3.rf_project_report_line rprl + INNER JOIN v3.rf_project_report rpr ON rpr.id = rprl.rf_project_report_id + WHERE rpr.project_id = p_project_id + ) THEN + RETURN true; + END IF; + + RETURN false; + + + END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.del_project(p_project_id integer) +RETURNS integer +LANGUAGE plpgsql +AS $function$ +DECLARE + v_old RECORD; + v_actor_id INTEGER; + v_role VARCHAR; +BEGIN + SELECT p.name, p.level, p.org_unit_id + INTO v_old + FROM v3.project p WHERE p.id = p_project_id; + IF NOT FOUND THEN + RAISE EXCEPTION 'project #% не существует', p_project_id; + END IF; + + IF NOT v3.can_delete_project(p_project_id) THEN + RAISE EXCEPTION 'project #% невозможно удалить', p_project_id; + END IF; + + -- Каскад вручную: rf_project_report_quarter / line / report (нет ON DELETE) + DELETE FROM v3.form3_phase f3p WHERE f3p.rf_project_report_id IN ( + SELECT id FROM v3.rf_project_report rpr WHERE project_id = p_project_id + ); + DELETE FROM v3.rf_project_report_quarter + WHERE rf_project_report_line_id IN ( + SELECT l.id FROM v3.rf_project_report_line l + JOIN v3.rf_project_report r ON r.id = l.rf_project_report_id + WHERE r.project_id = p_project_id); + DELETE FROM v3.rf_project_report_line + WHERE rf_project_report_id IN ( + SELECT id FROM v3.rf_project_report WHERE project_id = p_project_id); + DELETE FROM v3.rf_project_report WHERE project_id = p_project_id; + DELETE FROM v3.project WHERE id = p_project_id; + + PERFORM v3.log_event( + 'PROJECT_DELETE', 'PROJECT', + jsonb_build_object( + 'project_id', p_project_id, + 'name', v_old.name, + 'level', v_old.level, + 'org_unit_id', v_old.org_unit_id), null, null, v_old.org_unit_id); + + RETURN p_project_id; +END; +$function$ +; + + + +-- Форма 4 + +CREATE OR REPLACE FUNCTION v3.v_form4_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_justification character varying, col_internal_order character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cs_amount numeric, col_cs_reference character varying, col_cs_counterparty character varying, col_cs_deadline character varying, col_cs_comment character varying, col_cs_future_y1 numeric, col_cs_future_y2 numeric, col_cs_other_ssp numeric, col_cs_centralized_flag character varying, col_al_contract_ref character varying, col_al_purpose character varying, col_rsv_q1 numeric, col_rsv_q2 numeric, col_rsv_q3 numeric, col_rsv_q4 numeric, col_rsv_year numeric, col_rsv_just character varying, col_col_amount numeric, col_col_protocol character varying, col_col_note character varying, col_ckk_ceiling numeric, col_ckk_q1 numeric, col_ckk_q2 numeric, col_ckk_q3 numeric, col_ckk_q4 numeric, col_ckk_rf_schedule character varying, col_ckk_delivery_deadline character varying, col_ckk_procurement_plan character varying, col_ckk_procurement_method character varying, col_ckk_comment character varying, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_addenda character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_q1 numeric, col_cd_q2 numeric, col_cd_q3 numeric, col_cd_q4 numeric, col_cd_rf_schedule character varying, col_cd_vat_rate character varying, col_cd_exchange_rate numeric, col_cd_amount_foreign numeric, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_rf numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_ho numeric, col_q1_pay_rf numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_rf numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_ho numeric, col_q2_pay_rf numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_rf numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_ho numeric, col_q3_pay_rf numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_rf numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_ho numeric, col_q4_pay_rf numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, _sort_path integer[]) + LANGUAGE plpgsql + STABLE +AS $function$ +#variable_conflict use_column +DECLARE + s_plan BOOL; s_seq_d BOOL; s_appr BOOL; + s_cd BOOL; s_book BOOL; + s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; + s_cs BOOL; s_al BOOL; s_rsv BOOL; s_col BOOL; s_ckk BOOL; + s_need_ap BOOL; -- plan/seq/reserve нужны для approved + cp1..cp4 +BEGIN + s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); + s_seq_d := p_sections IS NULL OR 'seq_dfip' = ANY(p_sections); + s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); + s_cd := p_sections IS NULL OR 'contract' = ANY(p_sections); + s_book := p_sections IS NULL OR 'booking' = ANY(p_sections); + s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); + s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); + s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); + s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); + s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); + s_cs := p_sections IS NULL OR 'contract_summary' = ANY(p_sections); + s_al := p_sections IS NULL OR 'allocation' = ANY(p_sections); + s_rsv := p_sections IS NULL OR 'reserve' = ANY(p_sections); + s_col := p_sections IS NULL OR 'collegial' = ANY(p_sections); + s_ckk := p_sections IS NULL OR 'ckk' = ANY(p_sections); + s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; + + RETURN QUERY + WITH + tw AS ( + SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, + t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids + FROM v3.mv_expense_item_tree t + JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id + JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code + WHERE t.sheet = p_sheet + ), + page AS ( + SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.name AS bname, bl.vsp_id + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + ), + -- Conditional joins (gated by section flags) + jp AS (SELECT p.* FROM v3.plan p JOIN page pg ON p.line_id = pg.lid WHERE s_need_ap), + jsd AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='DFIP'), + jr AS (SELECT r.* FROM v3.reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_need_ap), + jcd AS (SELECT c.* FROM v3.contract_detail c JOIN page pg ON c.line_id = pg.lid WHERE s_cd), + jck AS (SELECT c.* FROM v3.ckk c JOIN page pg ON c.line_id = pg.lid WHERE s_book OR s_ckk), + jcs AS (SELECT c.* FROM v3.contract_summary c JOIN page pg ON c.line_id = pg.lid WHERE s_cs), + jal AS (SELECT a.* FROM v3.allocation a JOIN page pg ON a.line_id = pg.lid WHERE s_al), + jcol AS (SELECT c.* FROM v3.collegial_approval c JOIN page pg ON c.line_id = pg.lid WHERE s_col), + jq1 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q1 AND q.quarter=1), + jq2 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q2 AND q.quarter=2), + jq3 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q3 AND q.quarter=3), + jq4 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE (s_q4 OR s_tot) AND q.quarter=4), + + -- INPUT строки: enriched данные + input_base AS ( + SELECT + pg.lid, pg.eid, pg.bname, pg.vsp_id, + v.address AS vsp_addr, + bl_just.justification AS just, bl_just.internal_order AS io, + t.parent_item_id AS sc, t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, t.path AS tree_path, + -- plan + COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, + COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, + p.comment AS pcmt, + -- seq dfip / ssp_go + COALESCE(sd.adj_q1,0) AS dq1, COALESCE(sd.adj_q2,0) AS dq2, + COALESCE(sd.adj_q3,0) AS dq3, COALESCE(sd.adj_q4,0) AS dq4, + sd.justification AS djust, + -- reserve + COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, + COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, + r.justification AS rjust, + -- contract (расширено: q1-4, addenda, exchange_rate, amount_foreign) + cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.addenda AS cd_add, + cd.contract_date AS cd_dt, cd.subject AS cd_subj, cd.currency AS cd_cur, + cd.ceiling_amount AS cd_ceil, + cd.expenses_q1 AS cd_q1, cd.expenses_q2 AS cd_q2, cd.expenses_q3 AS cd_q3, cd.expenses_q4 AS cd_q4, + cd.rf_schedule AS cd_rfsch, + cd.vat_rate AS cd_vat, cd.exchange_rate AS cd_xr, cd.amount_foreign AS cd_af, + cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, + cd.act AS cd_act, cd.comment AS cd_cmt, + -- ckk (booking из expenses_q* + полный блок: ceiling, rf_schedule, …) + ck.expenses_q1 AS bk1, ck.expenses_q2 AS bk2, ck.expenses_q3 AS bk3, ck.expenses_q4 AS bk4, + ck.expenses_next_year_q1 AS bn1, ck.expenses_next_year_q2 AS bn2, + ck.expenses_next_year_q3 AS bn3, ck.expenses_next_year_q4 AS bn4, + ck.ceiling_amount AS ck_ceil, ck.rf_schedule AS ck_rfsch, + ck.delivery_deadline AS ck_dd, ck.procurement_plan AS ck_pp, + ck.procurement_method AS ck_pm, ck.comment AS ck_cmt, + -- contract_summary (Действующий договор) + cs.total_amount AS cs_amt, cs.reference AS cs_ref, cs.counterparty AS cs_cp, + cs.deadline AS cs_dl, cs.comment AS cs_cmt, + cs.future_payments_y1 AS cs_y1, cs.future_payments_y2 AS cs_y2, + cs.other_ssp_amount AS cs_oss, cs.centralized_flag AS cs_cflag, + -- allocation + al.contract_ref AS al_ref, al.allocation_purpose AS al_purp, + -- collegial + col.approved_amount AS col_amt, col.protocol_reference AS col_pr, col.note AS col_note, + -- Q1 + q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_rf AS q1_arf, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt, + q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, + q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, q1.payment_comment AS q1_pcmt, + q1.payment_act AS q1_pact, q1.booking_amount AS q1_book, + q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, + q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, + q1.transfer_to_economy AS q1_te, + q1.transfer_delay_acts AS q1_tda, q1.transfer_delay_procurement AS q1_tdp, + q1.transfer_economy_rf AS q1_terf, + q1.transfer_next_comment AS q1_tnc, q1.transfer_far_comment AS q1_tfc, + -- Q2 + q2.target_change AS q2_tc, q2.base_plan_correction AS q2_bc, q2.base_plan_correction_comment AS q2_bcc, + q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, + q2.plan_revision_comment AS q2_rcmt, + q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_rf AS q2_arf, q2.adj_reserve AS q2_arv, + q2.adj_comment AS q2_acmt, + q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, + q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf, + q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, + q2.booking_amount AS q2_book, + q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, + q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, q2.transfer_to_economy AS q2_te, + q2.transfer_delay_acts AS q2_tda, q2.transfer_delay_procurement AS q2_tdp, + q2.transfer_economy_rf AS q2_terf, + q2.transfer_next_comment AS q2_tnc, q2.transfer_far_comment AS q2_tfc, + -- Q3 + q3.target_change AS q3_tc, q3.base_plan_correction AS q3_bc, + q3.base_plan_correction_comment AS q3_bcc, + q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, + q3.plan_revision_comment AS q3_rcmt, + q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_rf AS q3_arf, q3.adj_reserve AS q3_arv, + q3.adj_comment AS q3_acmt, + q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, + q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf, + q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, + q3.booking_amount AS q3_book, + q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, + q3.transfer_to_q4 AS q3_tq4, q3.transfer_to_economy AS q3_te, + q3.transfer_delay_acts AS q3_tda, q3.transfer_delay_procurement AS q3_tdp, + q3.transfer_economy_rf AS q3_terf, q3.transfer_next_comment AS q3_tnc, + -- Q4 + q4.target_change AS q4_tc, q4.base_plan_correction AS q4_bc, + q4.base_plan_correction_comment AS q4_bcc, + q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, + q4.plan_revision_comment AS q4_rcmt, + q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_rf AS q4_arf, q4.adj_reserve AS q4_arv, + q4.adj_comment AS q4_acmt, + q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, + q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf, + q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, + q4.booking_amount AS q4_book, + q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod, + q4.transfer_to_economy AS q4_te, + -- approved per quarter + COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, + COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, + COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, + COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4 + FROM page pg + JOIN tw t ON t.id = pg.eid + LEFT JOIN v3.vsp v ON v.id = pg.vsp_id + LEFT JOIN v3.budget_line bl_just ON bl_just.id = pg.lid + LEFT JOIN jp p ON p.line_id = pg.lid + LEFT JOIN jsd sd ON sd.line_id = pg.lid + LEFT JOIN jr r ON r.line_id = pg.lid + LEFT JOIN jcd cd ON cd.line_id = pg.lid + LEFT JOIN jck ck ON ck.line_id = pg.lid + LEFT JOIN jcs cs ON cs.line_id = pg.lid + LEFT JOIN jal al ON al.line_id = pg.lid + LEFT JOIN jcol col ON col.line_id = pg.lid + LEFT JOIN jq1 q1 ON q1.line_id = pg.lid + LEFT JOIN jq2 q2 ON q2.line_id = pg.lid + LEFT JOIN jq3 q3 ON q3.line_id = pg.lid + LEFT JOIN jq4 q4 ON q4.line_id = pg.lid + ), + -- Computed: corrected/new plans + -- Excel: CH10 (cp1) = AM10 + SUM(CB:CE) − CF10 + -- DJ10 (np2) = SUM(AN10, CY10, DH10, CF10, DE10:DG10) + -- В Excel CY и CF — две отдельные колонки (CY = «Закрытие 1-го квартала / Перенос + -- во 2 кв», CF = плановый «Перенос во 2 кв» внутри корректировок 1 кв). В DB + -- хранится одно поле q1.transfer_to_q2 — оно работает «за двоих»: вычитается + -- из cp1 (как CF) и прибавляется к np2 (как CY). Допущение: CY = CF. + enriched AS ( + SELECT b.*, + -- Q1 corrected_plan = approved + adj_* − transfer_to_q2 + b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_arf,0)+COALESCE(b.q1_arv,0) + - COALESCE(b.q1_tq2,0) AS cp1, + COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, + -- Q2 new_plan = approved + transfer_q1 + revision_inc/seq + base_correction + target_change + b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) + COALESCE(b.q2_bc,0)+COALESCE(b.q2_tc,0) AS np2, + COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, + b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) + COALESCE(b.q3_bc,0)+COALESCE(b.q3_tc,0) AS np3, + COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, + b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) + COALESCE(b.q4_bc,0)+COALESCE(b.q4_tc,0) AS np4, + COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 + FROM input_base b + ), + final_input AS ( + SELECT e.*, + -- Q2 cp = new_plan + adj_* − transfer_to_q3 + e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_arf,0)+COALESCE(e.q2_arv,0) + - COALESCE(e.q2_tq3,0) AS cp2, + -- Q3 cp = new_plan + adj_* − transfer_to_q4 + e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_arf,0)+COALESCE(e.q3_arv,0) + - COALESCE(e.q3_tq4,0) AS cp3, + -- Q4 cp = new_plan + adj_* (нет дальнейшего переноса) + e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_arf,0)+COALESCE(e.q4_arv,0) AS cp4 + FROM enriched e + ), + + -- Aggregates per expense_item (для иерархии) + agg AS ( + SELECT bl.expense_item_id AS eid, + SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, + SUM(sd.adj_q1) AS sd1, SUM(sd.adj_q2) AS sd2, SUM(sd.adj_q3) AS sd3, SUM(sd.adj_q4) AS sd4, + SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, + SUM(ck.expenses_q1) AS bk1, SUM(ck.expenses_q2) AS bk2, SUM(ck.expenses_q3) AS bk3, SUM(ck.expenses_q4) AS bk4, + SUM(ck.expenses_next_year_q1) AS bn1, SUM(ck.expenses_next_year_q2) AS bn2, + SUM(ck.expenses_next_year_q3) AS bn3, SUM(ck.expenses_next_year_q4) AS bn4 + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.plan p ON p.line_id = bl.id AND s_need_ap + LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor='DFIP' AND s_need_ap + LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap + LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + AND (s_need_ap OR s_book) + GROUP BY bl.expense_item_id + ), + -- Per-quarter aggregates from blq + aq_q AS ( + SELECT bl.expense_item_id AS eid, q.quarter, + SUM(q.adj_current) AS ac, SUM(q.adj_ssp) AS as_v, SUM(q.adj_rf) AS arf, SUM(q.adj_reserve) AS arv, + SUM(q.payment_amount) AS pa, + SUM(q.payment_amount_ho) AS pho, SUM(q.payment_amount_rf) AS prf, + SUM(q.booking_amount) AS book, + SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, + SUM(q.actual_spod) AS spod, + SUM(q.transfer_to_q2) AS tq2, SUM(q.transfer_to_q3) AS tq3, SUM(q.transfer_to_q4) AS tq4, + SUM(q.transfer_to_economy) AS te, + -- Δ к approved для перехода к new_plan: revision_inc/seq + target_change + base_correction + -- (Excel: DJ10 = SUM(AN, CY, DH, CF, DE:DG)). Должно совпадать с INPUT-формулой np. + SUM(COALESCE(q.plan_revision_increase,0) + + COALESCE(q.plan_revision_sequester,0) + + COALESCE(q.target_change,0) + + COALESCE(q.base_plan_correction,0)) AS rev + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + JOIN v3.budget_line_quarter q ON q.line_id = bl.id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + GROUP BY bl.expense_item_id, q.quarter + ), + -- Tree-rollup + tw_agg AS ( + SELECT tw.id, + SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, + SUM(a.sd1) AS sd1, SUM(a.sd2) AS sd2, SUM(a.sd3) AS sd3, SUM(a.sd4) AS sd4, + SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, + SUM(a.bk1) AS bk1, SUM(a.bk2) AS bk2, SUM(a.bk3) AS bk3, SUM(a.bk4) AS bk4, + SUM(a.bn1) AS bn1, SUM(a.bn2) AS bn2, SUM(a.bn3) AS bn3, SUM(a.bn4) AS bn4 + FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) + GROUP BY tw.id + ), + tw_aq1 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.book) AS book, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=1 GROUP BY tw.id + ), + tw_aq2 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.book) AS book, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=2 GROUP BY tw.id + ), + tw_aq3 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.book) AS book, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=3 GROUP BY tw.id + ), + tw_aq4 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arf) AS arf, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.book) AS book, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod, + SUM(b.te) AS te, SUM(b.rev) AS rev + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=4 GROUP BY tw.id + ) + + -- ═══ Часть A: INPUT строки ═════════════════════════════════════════════ + SELECT * FROM ( + SELECT + 'INPUT'::VARCHAR, 3, f.lid::INT, + f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), + f.just, f.io, f.vsp_id, f.vsp_addr, + -- plan + CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, + CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, + CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, + -- seq dfip + CASE WHEN s_seq_d THEN f.dq1 END, CASE WHEN s_seq_d THEN f.dq2 END, + CASE WHEN s_seq_d THEN f.dq3 END, CASE WHEN s_seq_d THEN f.dq4 END, + CASE WHEN s_seq_d THEN f.dq1+f.dq2+f.dq3+f.dq4 END, CASE WHEN s_seq_d THEN f.djust END, + -- approved + CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, + CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, + CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, + -- contract_summary (Действующий договор) + CASE WHEN s_cs THEN f.cs_amt END, CASE WHEN s_cs THEN f.cs_ref END, + CASE WHEN s_cs THEN f.cs_cp END, CASE WHEN s_cs THEN f.cs_dl END, + CASE WHEN s_cs THEN f.cs_cmt END, + CASE WHEN s_cs THEN f.cs_y1 END, CASE WHEN s_cs THEN f.cs_y2 END, + CASE WHEN s_cs THEN f.cs_oss END, CASE WHEN s_cs THEN f.cs_cflag END, + -- allocation + CASE WHEN s_al THEN f.al_ref END, CASE WHEN s_al THEN f.al_purp END, + -- reserve (отдельный блок) + CASE WHEN s_rsv THEN f.rq1 END, CASE WHEN s_rsv THEN f.rq2 END, + CASE WHEN s_rsv THEN f.rq3 END, CASE WHEN s_rsv THEN f.rq4 END, + CASE WHEN s_rsv THEN f.rq1+f.rq2+f.rq3+f.rq4 END, CASE WHEN s_rsv THEN f.rjust END, + -- collegial + CASE WHEN s_col THEN f.col_amt END, CASE WHEN s_col THEN f.col_pr END, + CASE WHEN s_col THEN f.col_note END, + -- ckk полный + CASE WHEN s_ckk THEN f.ck_ceil END, + CASE WHEN s_ckk THEN f.bk1 END, CASE WHEN s_ckk THEN f.bk2 END, + CASE WHEN s_ckk THEN f.bk3 END, CASE WHEN s_ckk THEN f.bk4 END, + CASE WHEN s_ckk THEN f.ck_rfsch END, CASE WHEN s_ckk THEN f.ck_dd END, + CASE WHEN s_ckk THEN f.ck_pp END, CASE WHEN s_ckk THEN f.ck_pm END, + CASE WHEN s_ckk THEN f.ck_cmt END, + -- contract (расширено) + CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, + CASE WHEN s_cd THEN f.cd_add END, + CASE WHEN s_cd THEN f.cd_dt END, CASE WHEN s_cd THEN f.cd_subj END, + CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, + CASE WHEN s_cd THEN f.cd_q1 END, CASE WHEN s_cd THEN f.cd_q2 END, + CASE WHEN s_cd THEN f.cd_q3 END, CASE WHEN s_cd THEN f.cd_q4 END, + CASE WHEN s_cd THEN f.cd_rfsch END, + CASE WHEN s_cd THEN f.cd_vat END, + CASE WHEN s_cd THEN f.cd_xr END, CASE WHEN s_cd THEN f.cd_af END, + CASE WHEN s_cd THEN f.cd_dl END, + CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END, + CASE WHEN s_cd THEN f.cd_cmt END, + -- booking 2026 + CASE WHEN s_book THEN f.bk1 END, CASE WHEN s_book THEN f.bk2 END, + CASE WHEN s_book THEN f.bk3 END, CASE WHEN s_book THEN f.bk4 END, + -- booking 2027 + CASE WHEN s_book THEN f.bn1 END, CASE WHEN s_book THEN f.bn2 END, + CASE WHEN s_book THEN f.bn3 END, CASE WHEN s_book THEN f.bn4 END, + -- Q1 + CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, + CASE WHEN s_q1 THEN f.q1_arf END, + CASE WHEN s_q1 THEN f.q1_arv END, + CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, + CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, + CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, CASE WHEN s_q1 THEN f.q1_pcmt END, + CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN f.q1_book END, + CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, + CASE WHEN s_q1 THEN f.aq1 END, + CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.q1_book,0) END, -- residual_after_booking + CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, -- residual_after_actual + CASE WHEN s_q1 THEN f.q1_tq2 END, + CASE WHEN s_q1 THEN f.q1_tda END, CASE WHEN s_q1 THEN f.q1_tdp END, CASE WHEN s_q1 THEN f.q1_terf END, + CASE WHEN s_q1 THEN f.q1_tnc END, + CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, + CASE WHEN s_q1 THEN f.q1_tfc END, + CASE WHEN s_q1 THEN f.q1_te END, + -- total = "Закрытие квартала" = сумма transfer-колонок + CASE WHEN s_q1 THEN COALESCE(f.q1_tq2,0)+COALESCE(f.q1_tq3,0)+COALESCE(f.q1_tq4,0)+COALESCE(f.q1_te,0) END, + -- Q2 + CASE WHEN s_q2 THEN f.q2_tc END, CASE WHEN s_q2 THEN f.q2_bc END, CASE WHEN s_q2 THEN f.q2_bcc END, + CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, + CASE WHEN s_q2 THEN f.q2_rcmt END, + CASE WHEN s_q2 THEN f.np2 END, + CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, + CASE WHEN s_q2 THEN f.q2_arf END, + CASE WHEN s_q2 THEN f.q2_arv END, + CASE WHEN s_q2 THEN f.q2_acmt END, + CASE WHEN s_q2 THEN f.cp2 END, + CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, + CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END, + CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, + CASE WHEN s_q2 THEN f.q2_book END, + CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, + CASE WHEN s_q2 THEN f.aq2 END, + CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.q2_book,0) END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, + CASE WHEN s_q2 THEN f.q2_tq3 END, + CASE WHEN s_q2 THEN f.q2_tda END, CASE WHEN s_q2 THEN f.q2_tdp END, CASE WHEN s_q2 THEN f.q2_terf END, + CASE WHEN s_q2 THEN f.q2_tnc END, + CASE WHEN s_q2 THEN f.q2_tq4 END, + CASE WHEN s_q2 THEN f.q2_tfc END, + CASE WHEN s_q2 THEN f.q2_te END, + CASE WHEN s_q2 THEN COALESCE(f.q2_tq3,0)+COALESCE(f.q2_tq4,0)+COALESCE(f.q2_te,0) END, + -- Q3 + CASE WHEN s_q3 THEN f.q3_tc END, CASE WHEN s_q3 THEN f.q3_bc END, + CASE WHEN s_q3 THEN f.q3_bcc END, + CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, + CASE WHEN s_q3 THEN f.q3_rcmt END, + CASE WHEN s_q3 THEN f.np3 END, + CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, + CASE WHEN s_q3 THEN f.q3_arf END, + CASE WHEN s_q3 THEN f.q3_arv END, + CASE WHEN s_q3 THEN f.q3_acmt END, + CASE WHEN s_q3 THEN f.cp3 END, + CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, + CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END, + CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, + CASE WHEN s_q3 THEN f.q3_book END, + CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, + CASE WHEN s_q3 THEN f.aq3 END, + CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.q3_book,0) END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, + CASE WHEN s_q3 THEN f.q3_tq4 END, + CASE WHEN s_q3 THEN f.q3_tda END, CASE WHEN s_q3 THEN f.q3_tdp END, CASE WHEN s_q3 THEN f.q3_terf END, + CASE WHEN s_q3 THEN f.q3_tnc END, + CASE WHEN s_q3 THEN f.q3_te END, + CASE WHEN s_q3 THEN COALESCE(f.q3_tq4,0)+COALESCE(f.q3_te,0) END, + -- Q4 + CASE WHEN s_q4 THEN f.q4_tc END, CASE WHEN s_q4 THEN f.q4_bc END, + CASE WHEN s_q4 THEN f.q4_bcc END, + CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, + CASE WHEN s_q4 THEN f.q4_rcmt END, + CASE WHEN s_q4 THEN f.np4 END, + CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, + CASE WHEN s_q4 THEN f.q4_arf END, + CASE WHEN s_q4 THEN f.q4_arv END, + CASE WHEN s_q4 THEN f.q4_acmt END, + CASE WHEN s_q4 THEN f.cp4 END, + CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, + CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END, + CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, + CASE WHEN s_q4 THEN f.q4_book END, + CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, + CASE WHEN s_q4 THEN f.q4_spod END, + CASE WHEN s_q4 THEN f.aq4 END, + CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.q4_book,0) END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_q4 THEN f.q4_te END, + CASE WHEN s_q4 THEN COALESCE(f.q4_te,0) END, + -- totals + CASE WHEN s_tot THEN f.aq1+f.aq2+f.aq3+f.aq4 END, -- fact_year + + f.tree_path || ARRAY[f.lid::INT] AS _sort + FROM final_input f + + UNION ALL + + -- ═══ Часть B: Иерархия ════════════════════════════════════════════════ + SELECT + CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR, + t.depth, NULL::INT, + CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END, + CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END, + t.num_group_id, t.name, + NULL::VARCHAR, NULL::VARCHAR, -- justification, internal_order + NULL::INT, NULL::VARCHAR, + -- plan + CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END, + CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END, + CASE WHEN s_plan THEN COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) END, + NULL::VARCHAR, + -- seq dfip + CASE WHEN s_seq_d THEN ta.sd1 END, CASE WHEN s_seq_d THEN ta.sd2 END, + CASE WHEN s_seq_d THEN ta.sd3 END, CASE WHEN s_seq_d THEN ta.sd4 END, + CASE WHEN s_seq_d THEN COALESCE(ta.sd1,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sd4,0) END, + NULL::VARCHAR, + -- approved + CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) + +COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) + +COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) + +COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) END, + -- contract_summary (NULL на иерархии — текстовые) + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, + -- allocation + NULL::VARCHAR, NULL::VARCHAR, + -- reserve (q1-4 + year агрегаты, justification NULL) + CASE WHEN s_rsv THEN ta.sr1 END, CASE WHEN s_rsv THEN ta.sr2 END, + CASE WHEN s_rsv THEN ta.sr3 END, CASE WHEN s_rsv THEN ta.sr4 END, + CASE WHEN s_rsv THEN COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0) END, + NULL::VARCHAR, + -- collegial + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, + -- ckk полный (q1-4 агрегаты есть в ta.bk*, остальное NULL) + NULL::NUMERIC, + CASE WHEN s_ckk THEN ta.bk1 END, CASE WHEN s_ckk THEN ta.bk2 END, + CASE WHEN s_ckk THEN ta.bk3 END, CASE WHEN s_ckk THEN ta.bk4 END, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + -- contract (расширено) — все NULL на иерархии + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + NULL::DATE, NULL::VARCHAR, + NULL::VARCHAR, NULL::NUMERIC, + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, + NULL::VARCHAR, + NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, + NULL::VARCHAR, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + -- booking 2026 / 2027 + CASE WHEN s_book THEN ta.bk1 END, CASE WHEN s_book THEN ta.bk2 END, + CASE WHEN s_book THEN ta.bk3 END, CASE WHEN s_book THEN ta.bk4 END, + CASE WHEN s_book THEN ta.bn1 END, CASE WHEN s_book THEN ta.bn2 END, + CASE WHEN s_book THEN ta.bn3 END, CASE WHEN s_book THEN ta.bn4 END, + -- Q1 hierarchy + CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as_v END, + CASE WHEN s_q1 THEN tb1.arf END, + CASE WHEN s_q1 THEN tb1.arv END, + NULL::VARCHAR, + -- corrected_plan = approved + adj_* − transfer_to_q2 + CASE WHEN s_q1 THEN + COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) + +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0) + -COALESCE(tb1.tq2,0) + END, + NULL::DATE, + CASE WHEN s_q1 THEN tb1.pa END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, + NULL::VARCHAR, -- pay_act + CASE WHEN s_q1 THEN tb1.book END, -- booking агрегат + CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END, + CASE WHEN s_q1 THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) END, + -- residual_after_booking = cp − booking + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) + +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0) + -COALESCE(tb1.tq2,0)) + - COALESCE(tb1.book,0) + END, + -- residual_after_actual = cp − actual + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sr1,0) + +COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arf,0)+COALESCE(tb1.arv,0) + -COALESCE(tb1.tq2,0)) + - (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) + END, + CASE WHEN s_q1 THEN tb1.tq2 END, + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- tq2 reasons + next comment + CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END, + NULL::VARCHAR, -- far comment + CASE WHEN s_q1 THEN tb1.te END, + -- total = "Закрытие квартала" = сумма transfer-колонок + CASE WHEN s_q1 THEN + COALESCE(tb1.tq2,0)+COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)+COALESCE(tb1.te,0) + END, + -- Q2 hierarchy + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, -- revision_comment + CASE WHEN s_q2 THEN + COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) + END, + CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as_v END, + CASE WHEN s_q2 THEN tb2.arf END, + CASE WHEN s_q2 THEN tb2.arv END, + NULL::VARCHAR, -- adj_comment + -- corrected_plan = new_plan + adj_* − transfer_to_q3 + CASE WHEN s_q2 THEN + COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) + +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) + +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0) + -COALESCE(tb2.tq3,0) + END, + NULL::DATE, + CASE WHEN s_q2 THEN tb2.pa END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- pay_ho/pay_rf/pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q2 THEN tb2.book END, + CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END, + CASE WHEN s_q2 THEN COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) END, + -- residual_after_booking = cp − booking + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) + +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) + +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0) + -COALESCE(tb2.tq3,0)) + - COALESCE(tb2.book,0) + END, + -- residual_after_actual = cp − actual + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sr2,0) + +COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) + +COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arf,0)+COALESCE(tb2.arv,0) + -COALESCE(tb2.tq3,0)) + - (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) + END, + CASE WHEN s_q2 THEN tb2.tq3 END, + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- tq3 reasons + next comment + CASE WHEN s_q2 THEN tb2.tq4 END, + NULL::VARCHAR, -- far comment + CASE WHEN s_q2 THEN tb2.te END, + -- total = сумма transfer-колонок q2 + CASE WHEN s_q2 THEN + COALESCE(tb2.tq3,0)+COALESCE(tb2.tq4,0)+COALESCE(tb2.te,0) + END, + -- Q3 hierarchy + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, + CASE WHEN s_q3 THEN + COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) + END, + CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as_v END, + CASE WHEN s_q3 THEN tb3.arf END, + CASE WHEN s_q3 THEN tb3.arv END, + NULL::VARCHAR, + -- corrected_plan = new_plan + adj_* − transfer_to_q4 + CASE WHEN s_q3 THEN + COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) + +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) + +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0) + -COALESCE(tb3.tq4,0) + END, + NULL::DATE, + CASE WHEN s_q3 THEN tb3.pa END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, + NULL::VARCHAR, + CASE WHEN s_q3 THEN tb3.book END, + CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END, + CASE WHEN s_q3 THEN COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) END, + -- residual_after_booking = cp − booking + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) + +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) + +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0) + -COALESCE(tb3.tq4,0)) + - COALESCE(tb3.book,0) + END, + -- residual_after_actual = cp − actual + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sr3,0) + +COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) + +COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arf,0)+COALESCE(tb3.arv,0) + -COALESCE(tb3.tq4,0)) + - (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) + END, + CASE WHEN s_q3 THEN tb3.tq4 END, + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, + CASE WHEN s_q3 THEN tb3.te END, + -- total = сумма transfer-колонок q3 + CASE WHEN s_q3 THEN + COALESCE(tb3.tq4,0)+COALESCE(tb3.te,0) + END, + -- Q4 hierarchy + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, + CASE WHEN s_q4 THEN + COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + END, + CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as_v END, + CASE WHEN s_q4 THEN tb4.arf END, + CASE WHEN s_q4 THEN tb4.arv END, + NULL::VARCHAR, + -- corrected_plan = new_plan + adj_* (нет дальнейшего переноса) + CASE WHEN s_q4 THEN + COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) + +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0) + END, + NULL::DATE, + CASE WHEN s_q4 THEN tb4.pa END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, + NULL::VARCHAR, + CASE WHEN s_q4 THEN tb4.book END, + CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END, + CASE WHEN s_q4 THEN tb4.spod END, + CASE WHEN s_q4 THEN COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, + -- residual_after_booking = cp − booking + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) + +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0)) + - COALESCE(tb4.book,0) + END, + -- residual_after_actual = cp − actual + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sr4,0) + +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arf,0)+COALESCE(tb4.arv,0)) + - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) + END, + CASE WHEN s_q4 THEN tb4.te END, + -- total = "Закрытие" q4 = transfer_to_economy + CASE WHEN s_q4 THEN COALESCE(tb4.te,0) END, + -- totals + CASE WHEN s_tot THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) + +COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) + +COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) + +COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, -- fact_year + + t.path AS _sort + FROM tw t + LEFT JOIN tw_agg ta ON ta.id = t.id + LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id + LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id + LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id + LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id + ) sub + ORDER BY sub._sort; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form4_sheet_lines_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, approved_data jsonb, contract_summary_data jsonb, allocation_data jsonb, reserve_data jsonb, collegial_data jsonb, ckk_data jsonb, contract_data jsonb, booking_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[]) + LANGUAGE sql + STABLE +AS $function$ + WITH flags AS ( + SELECT + (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, + (p_sections IS NULL OR 'seq_dfip' = ANY(p_sections)) AS s_seq_d, + (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, + (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs, + (p_sections IS NULL OR 'allocation' = ANY(p_sections)) AS s_al, + (p_sections IS NULL OR 'reserve' = ANY(p_sections)) AS s_rsv, + (p_sections IS NULL OR 'collegial' = ANY(p_sections)) AS s_col, + (p_sections IS NULL OR 'ckk' = ANY(p_sections)) AS s_ckk, + (p_sections IS NULL OR 'contract' = ANY(p_sections)) AS s_cd, + (p_sections IS NULL OR 'booking' = ANY(p_sections)) AS s_book, + (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, + (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, + (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, + (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, + (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot + ) + SELECT + s.row_type, s.depth, s.line_id, + + -- header (всегда). expense_item_id из _sort_path (см. v_form1_jsonb). + jsonb_build_object( + 'section_code', s.col_section_code, + 'item_id', s.col_item_id, + 'num_group', s.col_num_group_id, + 'name', s.col_name, + 'justification', s.col_justification, + 'internal_order', s.col_internal_order, + 'vsp_id', s.col_vsp_id, + 'vsp_address', s.col_vsp_address, + 'expense_item_id', CASE WHEN s.row_type = 'INPUT' + THEN s._sort_path[array_upper(s._sort_path,1)-1] + ELSE s._sort_path[array_upper(s._sort_path,1)] END + ) AS header, + + CASE WHEN f.s_plan THEN jsonb_build_object( + 'q1', s.col_plan_q1, 'q2', s.col_plan_q2, + 'q3', s.col_plan_q3, 'q4', s.col_plan_q4, + 'year', s.col_plan_year, 'comment', s.col_plan_comment + ) END AS plan_data, + + CASE WHEN f.s_seq_d THEN jsonb_build_object( + 'q1', s.col_seq_dfip_q1, 'q2', s.col_seq_dfip_q2, + 'q3', s.col_seq_dfip_q3, 'q4', s.col_seq_dfip_q4, + 'year', s.col_seq_dfip_year, + 'justification', s.col_seq_dfip_just + ) END AS seq_dfip_data, + + CASE WHEN f.s_appr THEN jsonb_build_object( + 'q1', s.col_appr_q1, 'q2', s.col_appr_q2, + 'q3', s.col_appr_q3, 'q4', s.col_appr_q4, + 'year', s.col_appr_year + ) END AS approved_data, + + CASE WHEN f.s_cs THEN jsonb_build_object( + 'total', s.col_cs_amount, + 'reference', s.col_cs_reference, + 'counterparty', s.col_cs_counterparty, + 'deadline', s.col_cs_deadline, + 'comment', s.col_cs_comment, + 'future_y1', s.col_cs_future_y1, + 'future_y2', s.col_cs_future_y2, + 'other_ssp', s.col_cs_other_ssp, + 'centralized_flag', s.col_cs_centralized_flag + ) END AS contract_summary_data, + + CASE WHEN f.s_al THEN jsonb_build_object( + 'contract_ref', s.col_al_contract_ref, + 'allocation_purpose', s.col_al_purpose + ) END AS allocation_data, + + CASE WHEN f.s_rsv THEN jsonb_build_object( + 'q1', s.col_rsv_q1, 'q2', s.col_rsv_q2, + 'q3', s.col_rsv_q3, 'q4', s.col_rsv_q4, + 'year', s.col_rsv_year, + 'justification', s.col_rsv_just + + ) END AS reserve_data, + + CASE WHEN f.s_col THEN jsonb_build_object( + 'approved', s.col_col_amount, + 'protocol', s.col_col_protocol, + 'note', s.col_col_note + ) END AS collegial_data, + + CASE WHEN f.s_ckk THEN jsonb_build_object( + 'ceiling', s.col_ckk_ceiling, + 'q1', s.col_ckk_q1, + 'q2', s.col_ckk_q2, + 'q3', s.col_ckk_q3, + 'q4', s.col_ckk_q4, + 'rf_schedule', s.col_ckk_rf_schedule, + 'deadline', s.col_ckk_delivery_deadline, + 'proc_plan', s.col_ckk_procurement_plan, + 'proc_method', s.col_ckk_procurement_method, + 'comment', s.col_ckk_comment + ) END AS ckk_data, + + CASE WHEN f.s_cd THEN jsonb_build_object( + 'counterparty', s.col_cd_counterparty, + 'reference', s.col_cd_reference, + 'addenda', s.col_cd_addenda, + 'date', s.col_cd_date, + 'subject', s.col_cd_subject, + 'currency', s.col_cd_currency, + 'ceiling', s.col_cd_ceiling, + 'q1', s.col_cd_q1, + 'q2', s.col_cd_q2, + 'q3', s.col_cd_q3, + 'q4', s.col_cd_q4, + 'rf_schedule', s.col_cd_rf_schedule, + 'vat_rate', s.col_cd_vat_rate, + 'exchange_rate', s.col_cd_exchange_rate, + 'amount_foreign', s.col_cd_amount_foreign, + 'deadline', s.col_cd_deadline, + 'scheme', s.col_cd_scheme, + 'act', s.col_cd_act, + 'comment', s.col_cd_comment + ) END AS contract_data, + + CASE WHEN f.s_book THEN jsonb_build_object( + 'y2026', jsonb_build_object('q1', s.col_book_q1, 'q2', s.col_book_q2, 'q3', s.col_book_q3, 'q4', s.col_book_q4), + 'y2027', jsonb_build_object('q1', s.col_book_next_q1, 'q2', s.col_book_next_q2, 'q3', s.col_book_next_q3, 'q4', s.col_book_next_q4) + ) END AS booking_data, + + CASE WHEN f.s_q1 THEN jsonb_build_object( + 'adj_current', s.col_q1_adj_current, + 'adj_ssp', s.col_q1_adj_ssp, + 'adj_rf', s.col_q1_adj_rf, + 'adj_reserve', s.col_q1_adj_reserve, + 'adj_comment', s.col_q1_adj_comment, + 'corrected_plan', s.col_q1_corrected_plan, + 'pay_date', s.col_q1_pay_date, + 'pay_amount', s.col_q1_pay_amount, + 'pay_ho', s.col_q1_pay_ho, + 'pay_rf', s.col_q1_pay_rf, + 'pay_comment', s.col_q1_pay_comment, + 'pay_act', s.col_q1_pay_act, + 'booking', s.col_q1_booking, + 'actual_m1', s.col_q1_actual_m1, + 'actual_m2', s.col_q1_actual_m2, + 'actual_m3', s.col_q1_actual_m3, + 'actual_quarter', s.col_q1_actual_quarter, + 'residual_after_booking', s.col_q1_residual_after_booking, + 'residual_after_actual', s.col_q1_residual_after_actual, + 'transfer_q2', s.col_q1_transfer_q2, + 'transfer_q3', s.col_q1_transfer_q3, + 'transfer_q4', s.col_q1_transfer_q4, + 'transfer_econ', s.col_q1_transfer_econ, + 'total', s.col_q1_total + ) END AS q1_data, + + CASE WHEN f.s_q2 THEN jsonb_build_object( + 'target_change', s.col_q2_target_change, + 'base_correction', s.col_q2_base_correction, + 'base_correction_comment', s.col_q2_base_correction_comment, + 'revision_inc', s.col_q2_revision_inc, + 'revision_seq', s.col_q2_revision_seq, + 'revision_comment', s.col_q2_revision_comment, + 'new_plan', s.col_q2_new_plan, + 'adj_current', s.col_q2_adj_current, + 'adj_ssp', s.col_q2_adj_ssp, + 'adj_rf', s.col_q2_adj_rf, + 'adj_reserve', s.col_q2_adj_reserve, + 'adj_comment', s.col_q2_adj_comment, + 'corrected_plan', s.col_q2_corrected_plan, + 'pay_date', s.col_q2_pay_date, + 'pay_amount', s.col_q2_pay_amount, + 'pay_ho', s.col_q2_pay_ho, + 'pay_rf', s.col_q2_pay_rf, + 'pay_comment', s.col_q2_pay_comment, + 'pay_act', s.col_q2_pay_act, + 'booking', s.col_q2_booking, + 'actual_m1', s.col_q2_actual_m1, + 'actual_m2', s.col_q2_actual_m2, + 'actual_m3', s.col_q2_actual_m3, + 'actual_quarter', s.col_q2_actual_quarter, + 'residual_after_booking', s.col_q2_residual_after_booking, + 'residual_after_actual', s.col_q2_residual_after_actual, + 'transfer_q3', s.col_q2_transfer_q3, + 'transfer_q4', s.col_q2_transfer_q4, + 'transfer_econ', s.col_q2_transfer_econ, + 'total', s.col_q2_total + ) END AS q2_data, + + CASE WHEN f.s_q3 THEN jsonb_build_object( + 'target_change', s.col_q3_target_change, + 'base_correction', s.col_q3_base_correction, + 'base_correction_comment', s.col_q3_base_correction_comment, + 'revision_inc', s.col_q3_revision_inc, + 'revision_seq', s.col_q3_revision_seq, + 'revision_comment', s.col_q3_revision_comment, + 'new_plan', s.col_q3_new_plan, + 'adj_current', s.col_q3_adj_current, + 'adj_ssp', s.col_q3_adj_ssp, + 'adj_rf', s.col_q3_adj_rf, + 'adj_reserve', s.col_q3_adj_reserve, + 'adj_comment', s.col_q3_adj_comment, + 'corrected_plan', s.col_q3_corrected_plan, + 'pay_date', s.col_q3_pay_date, + 'pay_amount', s.col_q3_pay_amount, + 'pay_ho', s.col_q3_pay_ho, + 'pay_rf', s.col_q3_pay_rf, + 'pay_comment', s.col_q3_pay_comment, + 'pay_act', s.col_q3_pay_act, + 'booking', s.col_q3_booking, + 'actual_m1', s.col_q3_actual_m1, + 'actual_m2', s.col_q3_actual_m2, + 'actual_m3', s.col_q3_actual_m3, + 'actual_quarter', s.col_q3_actual_quarter, + 'residual_after_booking', s.col_q3_residual_after_booking, + 'residual_after_actual', s.col_q3_residual_after_actual, + 'transfer_q4', s.col_q3_transfer_q4, + 'transfer_econ', s.col_q3_transfer_econ, + 'total', s.col_q3_total + ) END AS q3_data, + + CASE WHEN f.s_q4 THEN jsonb_build_object( + 'target_change', s.col_q4_target_change, + 'base_correction', s.col_q4_base_correction, + 'base_correction_comment', s.col_q4_base_correction_comment, + 'revision_inc', s.col_q4_revision_inc, + 'revision_seq', s.col_q4_revision_seq, + 'revision_comment', s.col_q4_revision_comment, + 'new_plan', s.col_q4_new_plan, + 'adj_current', s.col_q4_adj_current, + 'adj_ssp', s.col_q4_adj_ssp, + 'adj_rf', s.col_q4_adj_rf, + 'adj_reserve', s.col_q4_adj_reserve, + 'adj_comment', s.col_q4_adj_comment, + 'corrected_plan', s.col_q4_corrected_plan, + 'pay_date', s.col_q4_pay_date, + 'pay_amount', s.col_q4_pay_amount, + 'pay_ho', s.col_q4_pay_ho, + 'pay_rf', s.col_q4_pay_rf, + 'pay_comment', s.col_q4_pay_comment, + 'pay_act', s.col_q4_pay_act, + 'booking', s.col_q4_booking, + 'actual_m1', s.col_q4_actual_m1, + 'actual_m2', s.col_q4_actual_m2, + 'actual_m3', s.col_q4_actual_m3, + 'actual_spod', s.col_q4_actual_spod, + 'actual_quarter', s.col_q4_actual_quarter, + 'residual_after_booking', s.col_q4_residual_after_booking, + 'residual_after_actual', s.col_q4_residual_after_actual, + 'transfer_econ', s.col_q4_transfer_econ, + 'total', s.col_q4_total + ) END AS q4_data, + + CASE WHEN f.s_tot THEN jsonb_build_object( + 'fact_year', s.col_fact_year + ) END AS totals_data, + + s._sort_path + FROM v3.v_form4_sheet_sections(p_form_id, p_sheet, p_sections) s + CROSS JOIN flags f + ORDER BY s._sort_path; +$function$ +; + + +CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE sql + STABLE +AS $function$ + -- ═══ 1. Базовые INPUT-строки с расширенной иерархией ═══════════════════════ + WITH inp_raw AS ( + SELECT j.* + FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j + WHERE j.row_type = 'INPUT' + ), + inp AS ( + SELECT + i.*, + bl.project_id AS line_project_id, + + -- --- разрешение проекта --- + -- A. bl.project_id → project (level='project') + -- B. bl.project_id → program (level='program') — тогда проекта нет + CASE WHEN prj.id IS NOT NULL THEN prj.id END AS prj_id, + CASE WHEN prj.id IS NOT NULL THEN prj.name END AS prj_name, + + -- --- разрешение программы --- + -- A. через проект: prog_via_prj + -- B. напрямую: prog_direct + COALESCE(prog_via_prj.id, prog_direct.id) AS prog_id, + COALESCE(prog_via_prj.name, prog_direct.name) AS prog_name, + + -- --- section (depth=0) --- + sec.id AS sec_id, + sec.section_code AS sec_code, + sec.name AS sec_name + + FROM inp_raw i + JOIN v3.budget_line bl ON bl.id = i.line_id + + -- проект (level='project') + LEFT JOIN v3.form4_project prj + ON prj.id = bl.project_id AND prj.level = 'project' + + -- программа через проект + LEFT JOIN v3.form4_project prog_via_prj + ON prog_via_prj.id = prj.parent_id AND prog_via_prj.level = 'program' + + -- программа напрямую (bl.project_id → program) + LEFT JOIN v3.form4_project prog_direct + ON prog_direct.id = bl.project_id AND prog_direct.level = 'program' + + -- раздел depth=0 (через expense_item) + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN LATERAL ( + WITH RECURSIVE up AS ( + SELECT id, parent_id, name, section_code, depth + FROM v3.expense_item WHERE id = ei.id + UNION ALL + SELECT e.id, e.parent_id, e.name, e.section_code, e.depth + FROM v3.expense_item e JOIN up u ON u.parent_id = e.id + ) + SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1 + ) sec ON TRUE + ), + tp_agg AS ( + SELECT 1 AS tp_id, + jsonb_build_object( + 'q1', 0, 'q2', 0, + 'q3', 0, 'q4', 0, + 'year', 0, 'comment', null + ) AS tp_plan, + + + + jsonb_build_object( + 'q1', 0, 'q2', 0, + 'q3', 0, 'q4', 0, + 'year', 0, + 'justification', null + ) AS tp_seq_dfip, + + jsonb_build_object( + 'q1', 0, 'q2', 0, + 'q3', 0, 'q4', 0, + 'year', 0 + ) AS tp_approved, + + jsonb_build_object( + 'total', null, + 'reference', null, + 'counterparty', null, + 'deadline', null, + 'comment', null, + 'future_y1', null, + 'future_y2', null, + 'other_ssp', null, + 'centralized_flag', null + ) AS tp_contract_summary, + + jsonb_build_object( + 'contract_ref', null, + 'allocation_purpose', null + ) AS tp_allocation, + + jsonb_build_object( + 'q1', 0, 'q2', 0, + 'q3', 0, 'q4', 0, + 'year', 0, + 'justification', null + ) AS tp_reserve, + + jsonb_build_object( + 'approved', null, + 'protocol', null, + 'note', null + ) AS tp_collegial, + + jsonb_build_object( + 'ceiling', null, + 'q1', null, + 'q2', null, + 'q3', null, + 'q4', null, + 'rf_schedule', null, + 'delivery_deadline', null, + 'proc_plan', null, + 'proc_method', null, + 'comment', null + ) AS tp_ckk, + + jsonb_build_object( + 'act', null, + 'date', null, + 'scheme', null, + 'addenda', null, + 'ceiling', null, + 'comment', null, + 'subject', null, + 'currency', null, + 'deadline', null, + 'vat_rate', null, + 'reference', null, + 'q1', null, + 'q2', null, + 'q3', null, + 'q4', null, + 'rf_schedule', null, + 'counterparty', null, + 'exchange_rate', null, + 'amount_foreign', null + ) AS tp_contract, + + jsonb_build_object( + 'y2026', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null), + 'y2027', jsonb_build_object('q1', null, 'q2', null, 'q3', null, 'q4', null) + ) AS tp_booking, + + jsonb_build_object( + 'total', 0, + 'adj_rf', null, + 'pay_ho', null, + 'pay_rf', null, + 'adj_ssp', null, + 'booking', null, + 'pay_act', null, + 'pay_date', null, + 'actual_m1', null, + 'actual_m2', null, + 'actual_m3', null, + 'pay_amount', null, + 'adj_comment', null, + 'adj_current', null, + 'adj_reserve', null, + 'pay_comment', null, + 'transfer_q2', null, + 'transfer_q3', null, + 'transfer_q4', null, + 'transfer_econ', null, + 'actual_quarter', 0, + 'corrected_plan', 0, + 'residual_after_actual', 0, + 'residual_after_booking', 0 + ) AS tp_q1, + + jsonb_build_object( + 'target_change', null, + 'base_correction', null, + 'base_correction_comment', null, + 'revision_inc', null, + 'revision_seq', null, + 'revision_comment', null, + 'new_plan', 0, + 'adj_current', null, + 'adj_ssp', null, + 'adj_rf', null, + 'adj_reserve', null, + 'adj_comment', null, + 'corrected_plan', 0, + 'pay_date', null, + 'pay_amount', null, + 'pay_ho', null, + 'pay_rf', null, + 'pay_comment', null, + 'pay_act', null, + 'booking', null, + 'actual_m1', null, + 'actual_m2', null, + 'actual_m3', null, + 'actual_quarter', 0, + 'residual_after_booking', 0, + 'residual_after_actual', 0, + 'transfer_q3', null, + 'transfer_q4', null, + 'transfer_econ', null, + 'total', 0 + ) AS tp_q2, + + jsonb_build_object( + 'target_change', null, + 'base_correction', null, + 'base_correction_comment', null, + 'revision_inc', null, + 'revision_seq', null, + 'revision_comment', null, + 'new_plan', 0, + 'adj_current', null, + 'adj_ssp', null, + 'adj_rf', null, + 'adj_reserve', null, + 'adj_comment', null, + 'corrected_plan', 0, + 'pay_date', null, + 'pay_amount', null, + 'pay_ho', null, + 'pay_rf', null, + 'pay_comment', null, + 'pay_act', null, + 'booking', null, + 'actual_m1', null, + 'actual_m2', null, + 'actual_m3', null, + 'actual_quarter', 0, + 'residual_after_booking', 0, + 'residual_after_actual', 0, + 'transfer_q4', null, + 'transfer_econ', null, + 'total', 0 + ) AS tp_q3, + + jsonb_build_object( + 'target_change', null, + 'base_correction', null, + 'base_correction_comment', null, + 'revision_inc', null, + 'revision_seq', null, + 'revision_comment', null, + 'new_plan', 0, + 'adj_current', null, + 'adj_ssp', null, + 'adj_rf', null, + 'adj_reserve', null, + 'adj_comment', null, + 'corrected_plan', 0, + 'pay_date', null, + 'pay_amount', null, + 'pay_ho', null, + 'pay_rf', null, + 'pay_comment', null, + 'pay_act', null, + 'booking', null, + 'actual_m1', null, + 'actual_m2', null, + 'actual_m3', null, + 'actual_spod', null, + 'actual_quarter', 0, + 'residual_after_booking', 0, + 'residual_after_actual', 0, + 'transfer_econ', null, + 'total', 0 + ) AS tp_q4, + jsonb_build_object( + 'fact_year', 0 + ) AS tp_totals + + + + + + ), + -- ═══ 2. Все программы (включая пустые) ═════════════════════════════════════ + all_programs AS ( + SELECT + fp.id AS prog_id, + fp.name AS prog_name, + fp.section_code, + sec.id AS sec_id, + sec.section_code AS sec_code, + sec.name AS sec_name + FROM v3.form4_project fp + LEFT JOIN LATERAL ( + SELECT id, section_code, name + FROM v3.expense_item + WHERE section_code = fp.section_code AND depth = 0 + LIMIT 1 + ) sec ON TRUE + WHERE fp.level = 'program' + AND (p_sections IS NULL OR fp.section_code = ANY(p_sections)) + AND (fp.section_code = p_sheet) AND fp.form_id = p_form_id + ), + + -- ═══ 3. Все проекты под программами (включая пустые) ═══════════════════════ + all_projects AS ( + SELECT + prj.id AS prj_id, + prj.name AS prj_name, + prj.parent_id AS prog_id, + ap.sec_id, ap.sec_code, ap.sec_name + FROM v3.form4_project prj + JOIN all_programs ap ON ap.prog_id = prj.parent_id + WHERE prj.level = 'project' AND (ap.section_code = p_sheet) + ), + + -- ═══ 4. Агрегаты с финансовыми данными (только из inp) ════════════════════ + + -- PROJECT-агрегаты + prj_agg AS ( + SELECT + sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name, + prog_id, MAX(prog_name) AS prog_name, + prj_id, MAX(prj_name) AS prj_name, + v3.jsonb_sum(plan_data) AS plan_data, + v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data, + v3.jsonb_sum(approved_data) AS approved_data, + v3.jsonb_sum(contract_summary_data) AS contract_summary_data, + v3.jsonb_sum(allocation_data) AS allocation_data, + v3.jsonb_sum(reserve_data) AS reserve_data, + v3.jsonb_sum(collegial_data) AS collegial_data, + v3.jsonb_sum(ckk_data) AS ckk_data, + v3.jsonb_sum(contract_data) AS contract_data, + v3.jsonb_sum(booking_data) AS booking_data, + v3.jsonb_sum(q1_data) AS q1_data, + v3.jsonb_sum(q2_data) AS q2_data, + v3.jsonb_sum(q3_data) AS q3_data, + v3.jsonb_sum(q4_data) AS q4_data, + v3.jsonb_sum(totals_data) AS totals_data + FROM inp WHERE prj_id IS NOT NULL + GROUP BY sec_id, prog_id, prj_id + ), + + -- PROGRAM-агрегаты + prog_agg AS ( + SELECT + sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name, + prog_id, MAX(prog_name) AS prog_name, + v3.jsonb_sum(plan_data) AS plan_data, + v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data, + v3.jsonb_sum(approved_data) AS approved_data, + v3.jsonb_sum(contract_summary_data) AS contract_summary_data, + v3.jsonb_sum(allocation_data) AS allocation_data, + v3.jsonb_sum(reserve_data) AS reserve_data, + v3.jsonb_sum(collegial_data) AS collegial_data, + v3.jsonb_sum(ckk_data) AS ckk_data, + v3.jsonb_sum(contract_data) AS contract_data, + v3.jsonb_sum(booking_data) AS booking_data, + v3.jsonb_sum(q1_data) AS q1_data, + v3.jsonb_sum(q2_data) AS q2_data, + v3.jsonb_sum(q3_data) AS q3_data, + v3.jsonb_sum(q4_data) AS q4_data, + v3.jsonb_sum(totals_data) AS totals_data + FROM inp WHERE prog_id IS NOT NULL + GROUP BY sec_id, prog_id + ), + + -- ROOT-агрегаты + root_agg AS ( + SELECT + sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name, + v3.jsonb_sum(plan_data) AS plan_data, + v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data, + v3.jsonb_sum(approved_data) AS approved_data, + v3.jsonb_sum(contract_summary_data) AS contract_summary_data, + v3.jsonb_sum(allocation_data) AS allocation_data, + v3.jsonb_sum(reserve_data) AS reserve_data, + v3.jsonb_sum(collegial_data) AS collegial_data, + v3.jsonb_sum(ckk_data) AS ckk_data, + v3.jsonb_sum(contract_data) AS contract_data, + v3.jsonb_sum(booking_data) AS booking_data, + v3.jsonb_sum(q1_data) AS q1_data, + v3.jsonb_sum(q2_data) AS q2_data, + v3.jsonb_sum(q3_data) AS q3_data, + v3.jsonb_sum(q4_data) AS q4_data, + v3.jsonb_sum(totals_data) AS totals_data + FROM inp + GROUP BY sec_id + ), + + -- Нулевой JSONB-шаблон (для пустых программ/проектов) + zero AS ( + SELECT '{}'::jsonb AS z + ), + + -- ═══ 5. UNION всех уровней ═════════════════════════════════════════════════ + unioned AS ( + -- ROOT — только из агрегатов (не имеет смысла пустой ROOT без строк) + SELECT + 'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id, + jsonb_build_object('section_code', sec_code, 'name', sec_name, 'expense_item_id', sec_id) AS header, + plan_data, seq_dfip_data, approved_data, + contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, + contract_data, booking_data, + q1_data, q2_data, q3_data, q4_data, totals_data, + ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path + FROM root_agg + + UNION ALL + + -- GROUP — из агрегатов (программы с бюджетными строками) + SELECT + 'GROUP', 1, NULL, + jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id), + plan_data, seq_dfip_data, approved_data, + contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, + contract_data, booking_data, + q1_data, q2_data, q3_data, q4_data, totals_data, + ARRAY[sec_id, prog_id, 0, 0]::INT[] + FROM prog_agg + + UNION ALL + + -- GROUP — пустые программы (нет ни одной budget_line) + SELECT + 'GROUP', 1, NULL, + jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id), + tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved + tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve + tp_collegial, tp_ckk, -- collegial, ckk + tp_contract, tp_booking, -- contract, booking + tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals + ARRAY[sec_id, prog_id, 0, 0]::INT[] + FROM all_programs ap + JOIN tp_agg ON tp_agg.tp_id = 1 + WHERE NOT EXISTS ( + SELECT 1 FROM v3.budget_line bl + WHERE bl.budget_form_id = p_form_id + AND ( + bl.project_id = ap.prog_id -- прямая привязка к программе + OR bl.project_id IN (SELECT id FROM v3.form4_project pr -- через проект + WHERE pr.parent_id = ap.prog_id AND pr.level = 'project') + ) + ) + + UNION ALL + + -- ITEM — из агрегатов (проекты с бюджетными строками) + SELECT + 'ITEM', 2, NULL, + jsonb_build_object('section_code', sec_code, 'name', prj_name, + 'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id), + plan_data, seq_dfip_data, approved_data, + contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, + contract_data, booking_data, + q1_data, q2_data, q3_data, q4_data, totals_data, + ARRAY[sec_id, prog_id, prj_id, 0]::INT[] + FROM prj_agg + + UNION ALL + + -- ITEM — пустые проекты (нет budget_line) + SELECT + 'ITEM', 2, NULL, + jsonb_build_object('section_code', sec_code, 'name', prj_name, + 'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id), + tp_plan, tp_seq_dfip, tp_approved, -- plan, seq_dfip, approved + tp_contract_summary, tp_allocation, tp_reserve, -- contract_summary, allocation, reserve + tp_collegial, tp_ckk, -- collegial, ckk + tp_contract, tp_booking, -- contract, booking + tp_q1, tp_q2, tp_q3, tp_q4, tp_totals, -- q1-q4, totals + ARRAY[sec_id, prog_id, prj_id, 0]::INT[] + FROM all_projects apr + JOIN tp_agg ON tp_agg.tp_id = 1 + WHERE NOT EXISTS ( + SELECT 1 FROM v3.budget_line bl + WHERE bl.budget_form_id = p_form_id + AND bl.project_id = apr.prj_id + ) + + UNION ALL + + -- INPUT — все строки бюджета + SELECT + 'INPUT', 3, line_id, + header || jsonb_build_object( + 'project_id', prj_id, 'project_name', prj_name, + 'program_id', prog_id, 'program_name', prog_name + ), + plan_data, seq_dfip_data, approved_data, + contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data, + contract_data, booking_data, + q1_data, q2_data, q3_data, q4_data, totals_data, + ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(CASE WHEN prj_id is null and prog_id is not null THEN 0 ELSE prj_id END, 999999), line_id]::INT[] + FROM inp + ) + + SELECT + u.row_type, u.depth, + ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order, + jsonb_build_object( + 'sort_path', u.sort_path, + 'line_id', u.line_id, + 'header', u.header, + 'plan', u.plan_data, + 'seq_dfip', u.seq_dfip_data, + 'approved', u.approved_data, + 'contract_summary', u.contract_summary_data, + 'allocation', u.allocation_data, + 'reserve', u.reserve_data, + 'collegial', u.collegial_data, + 'ckk', u.ckk_data, + 'contract', u.contract_data, + 'booking', u.booking_data, + 'q1', u.q1_data, + 'q2', u.q2_data, + 'q3', u.q3_data, + 'q4', u.q4_data, + 'totals', u.totals_data + ) AS data + FROM unioned u + ORDER BY u.sort_path; +$function$ +; + +CREATE OR REPLACE FUNCTION v3._apply_form_cell(p_line_id integer, p_column text, p_value jsonb, p_sheet character varying DEFAULT NULL::character varying, p_form_id integer DEFAULT NULL::integer) + RETURNS void + LANGUAGE plpgsql +AS $function$ +DECLARE + v_parts TEXT[]; + v_scope TEXT; + v_field TEXT; + v_q SMALLINT; + v_actor TEXT; + v_target_table TEXT; + v_target_col TEXT; + v_target_type TEXT; + v_key_kind TEXT; + v_str TEXT; + v_sql TEXT; + v_sat_table TEXT; + v_cnt INT; +BEGIN + v_parts := string_to_array(p_column, '.'); + + -- ═══ OTCH9F (fixed_asset_report) ══════════════════════════════════════ + -- p_line_id = fixed_asset_report.id. + -- Editable: b_604.{acquired,disposed}_*, b_60415.{acquired,transferred}_*, + -- totals.go_balance_only; + -- b_604.opening_*, b_60415.opening_* — ТОЛЬКО для month=1 (для + -- месяцев 2..12 это computed накопительно из месяца 1). + -- RO: equipment_name (нормативная номенклатура), month/expense_item_id/ + -- item_id/section_code (ключ строки), totals.{acquired_total, + -- disposed_total, balance_qty, balance_amt} (computed). + IF p_sheet = 'OTCH9F' THEN + DECLARE v_far_month SMALLINT; + BEGIN + SELECT month INTO v_far_month FROM v3.fixed_asset_report WHERE id = p_line_id; + IF v_far_month IS NULL THEN + RAISE EXCEPTION 'fixed_asset_report.id=% не существует', p_line_id; + END IF; + + -- Одноуровневые ключи — только нормативные/ключевые → RO + IF array_length(v_parts,1) = 1 THEN + IF v_parts[1] = 'equipment_name' THEN + RAISE EXCEPTION 'normative_field: equipment_name (нормативная номенклатура, правится отдельным API)'; + ELSIF v_parts[1] IN ('month','expense_item_id','item_id','section_code','id') THEN + RAISE EXCEPTION 'key_field: % (часть ключа строки, RO)', v_parts[1]; + ELSE + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + END IF; + + IF array_length(v_parts,1) <> 2 THEN + RAISE EXCEPTION 'bad_column_format: %', p_column; + END IF; + + -- Двухуровневые: totals.x / b_604.x / b_60415.x + IF v_parts[1] = 'totals' THEN + IF v_parts[2] = 'go_balance_only' THEN + v_target_col := 'go_balance_only_amt'; v_target_type := 'NUMERIC'; + ELSIF v_parts[2] IN ('acquired_total','disposed_total','balance_qty','balance_amt') THEN + RAISE EXCEPTION 'computed_field: totals.%', v_parts[2]; + ELSE + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + ELSIF v_parts[1] IN ('b_604','b_60415') THEN + -- opening_* для месяцев 2..12 — computed + IF v_parts[2] LIKE 'opening_%' AND v_far_month <> 1 THEN + RAISE EXCEPTION 'computed_field: % (opening для month>1 вычисляется из base + накопит. дельты)', p_column; + END IF; + -- Маппинг b_604/60415 → колонки таблицы + IF v_parts[1] = 'b_604' THEN + v_target_col := CASE v_parts[2] + WHEN 'opening_qty' THEN 'opening_qty_604' + WHEN 'opening_amt' THEN 'opening_amt_604' + WHEN 'acquired_qty' THEN 'acquired_qty_604' + WHEN 'acquired_amt' THEN 'acquired_amt_604' + WHEN 'disposed_qty' THEN 'disposed_qty_604' + WHEN 'disposed_amt' THEN 'disposed_amt_604' + END; + IF v_target_col IS NULL THEN + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + v_target_type := CASE WHEN v_parts[2] LIKE '%_qty' THEN 'INTEGER' ELSE 'NUMERIC' END; + ELSE -- b_60415 + v_target_col := CASE v_parts[2] + WHEN 'opening_qty' THEN 'opening_qty_60415' + WHEN 'opening_amt' THEN 'opening_amt_60415' + WHEN 'acquired_qty' THEN 'acquired_qty_60415' + WHEN 'acquired_amt' THEN 'acquired_amt_60415' + WHEN 'transferred_qty' THEN 'transferred_qty_60415' + WHEN 'transferred_amt' THEN 'transferred_amt_60415' + END; + IF v_target_col IS NULL THEN + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + v_target_type := CASE WHEN v_parts[2] LIKE '%_qty' THEN 'INTEGER' ELSE 'NUMERIC' END; + END IF; + ELSE + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + + EXECUTE format( + 'UPDATE v3.fixed_asset_report SET %I = ($2 #>> ''{}'')::%s WHERE id = $1', + v_target_col, v_target_type + ) USING p_line_id, p_value; + RETURN; + END; + END IF; + + -- ═══ AHR_LIMIT (limit_template + form_limit) ═════════════════════════ + -- p_line_id здесь = limit_template.id LEAF (глобальный каталог нормативов). + -- Editable: qty_q1..q4 (INTEGER), comment (TEXT) — пишутся в v3.form_limit + -- (UPSERT по (budget_form_id, template_id)). + -- amount_q1..q4 — computed по формуле в read (qty × limit × period_factor). + -- name/unit/limit_*/section_no/expense_item_code — нормативный справочник + -- (правится отдельным админ-API, не через write UI). + IF p_sheet = 'AHR_LIMIT' THEN + IF p_form_id IS NULL THEN + RAISE EXCEPTION 'internal: p_form_id не передан для AHR_LIMIT'; + END IF; + + -- Sanity: строка должна быть LEAF + IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt + WHERE lt.id = p_line_id AND lt.row_type = 'LEAF') THEN + RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF (SECTION/GROUP не редактируется)', p_line_id; + END IF; + + IF array_length(v_parts,1) <> 1 THEN + RAISE EXCEPTION 'bad_column_format: %, expected single key', p_column; + END IF; + + CASE v_parts[1] + WHEN 'qty_q1','qty_q2','qty_q3','qty_q4' THEN + v_target_col := v_parts[1]; v_target_type := 'INTEGER'; + WHEN 'comment' THEN + v_target_col := 'comment'; v_target_type := 'TEXT'; + WHEN 'amount_q1','amount_q2','amount_q3','amount_q4' THEN + RAISE EXCEPTION 'computed_field: % (вычисляется из qty × limit × period_factor)', p_column; + WHEN 'name','unit','section_no','expense_item_code', + 'limit_with_vat','limit_without_vat' THEN + RAISE EXCEPTION 'normative_field: % (нормативный справочник, правится админом отдельно)', p_column; + ELSE + RAISE EXCEPTION 'unknown_column: %', p_column; + END CASE; + + EXECUTE format( + 'INSERT INTO v3.form_limit (budget_form_id, template_id, %1$I) ' + || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) ' + || 'ON CONFLICT (budget_form_id, template_id) DO UPDATE SET %1$I = EXCLUDED.%1$I', + v_target_col, v_target_type + ) USING p_form_id, p_line_id, p_value; + RETURN; + END IF; + + -- ═══ Сателлиты (AHR_RENT / AHR_UTILITY / AHR_SECURITY) ═══════════════ + -- Здесь p_line_id трактуется как id строки сателлита (rent/utility/ + -- security_detail.id) — НЕ budget_line.id. UPDATE по этому id; INSERT/ + -- DELETE — через add_budget_line / del_budget_line с тем же sheet. + IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN + v_sat_table := CASE p_sheet + WHEN 'AHR_RENT' THEN 'rent_detail' + WHEN 'AHR_UTILITY' THEN 'utility_detail' + WHEN 'AHR_SECURITY' THEN 'security_detail' + END; + + -- Парсим (ключи бывают одно- и двухуровневые) + IF array_length(v_parts,1) = 1 THEN + CASE v_parts[1] + WHEN 'contract_number' THEN v_target_col := 'contract_number'; v_target_type := 'TEXT'; + WHEN 'contract_end_date' THEN v_target_col := 'contract_end_date'; v_target_type := 'DATE'; + WHEN 'comment' THEN v_target_col := 'comment'; v_target_type := 'TEXT'; + WHEN 'address','object_type','rented_area','object_area' THEN + RAISE EXCEPTION 'computed_field: % (атрибут v3.vsp, правится отдельно)', p_column; + ELSE RAISE EXCEPTION 'unknown_column: %', p_column; + END CASE; + ELSIF array_length(v_parts,1) = 2 AND v_parts[1] = 'plan' THEN + IF v_parts[2] = 'year' THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; + IF v_parts[2] NOT IN ('q1','q2','q3','q4') THEN RAISE EXCEPTION 'unknown_column: %', p_column; END IF; + v_target_col := 'plan_' || v_parts[2]; + v_target_type := 'NUMERIC'; + ELSIF array_length(v_parts,1) = 2 AND v_parts[1] LIKE 'fact_q%' THEN + IF v_parts[2] = 'total' THEN RAISE EXCEPTION 'computed_field: %', p_column; END IF; + -- белый список месяцев по соответствующему кварталу + IF (v_parts[1]='fact_q1' AND v_parts[2] NOT IN ('jan','feb','mar')) OR + (v_parts[1]='fact_q2' AND v_parts[2] NOT IN ('apr','may','jun')) OR + (v_parts[1]='fact_q3' AND v_parts[2] NOT IN ('jul','aug','sep')) OR + (v_parts[1]='fact_q4' AND v_parts[2] NOT IN ('oct','nov','dec')) THEN + RAISE EXCEPTION 'unknown_column: % (месяц вне квартала)', p_column; + END IF; + v_target_col := 'actual_' || v_parts[2]; + v_target_type := 'NUMERIC'; + ELSE + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + + EXECUTE format( + 'UPDATE v3.%I SET %I = ($2 #>> ''{}'')::%s WHERE id = $1', + v_sat_table, v_target_col, v_target_type + ) USING p_line_id, p_value; + + GET DIAGNOSTICS v_cnt = ROW_COUNT; + IF v_cnt = 0 THEN + RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_line_id; + END IF; + RETURN; + END IF; + + -- ═══ Основная сетка (FORM_1/2/4 AHR/CAP/OPER) ════════════════════════ + IF array_length(v_parts, 1) < 2 THEN + RAISE EXCEPTION 'bad_column_format: %, expected scope.field', p_column; + END IF; + v_scope := v_parts[1]; + v_field := v_parts[2]; + + -- ── header: editable budget_line columns ────────────────────────────── + IF v_scope = 'header' THEN + IF v_field = 'name' THEN + UPDATE v3.budget_line SET name = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id; + ELSIF v_field = 'internal_order' THEN + UPDATE v3.budget_line SET internal_order = (p_value #>> '{}'), updated_at = now() WHERE id = p_line_id; + ELSIF v_field = 'vsp_id' THEN + UPDATE v3.budget_line SET vsp_id = (p_value #>> '{}')::INT, updated_at = now() WHERE id = p_line_id; + ELSIF v_field IN ('year','section','item_id','num_group','vsp_address') THEN + RAISE EXCEPTION 'computed_field: %', p_column; + ELSE + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + RETURN; + END IF; + + -- ── computed scopes (RO) ────────────────────────────────────────────── + IF v_scope IN ('approved','totals') THEN + RAISE EXCEPTION 'computed_field: %', p_column; + END IF; + + -- ── booking.y{2026|2027}.qN → v3.ckk (см. read FORM_2/4 jsonb) ──────── + -- В read-обёртках booking синтезируется из ckk.expenses_qN (= y2026, + -- текущий год) и ckk.expenses_next_year_qN (= y2027, следующий). + -- Имена y2026/y2027 захардкожены в read; повторяем то же в write. + IF v_scope = 'booking' THEN + IF array_length(v_parts, 1) <> 3 THEN + RAISE EXCEPTION 'bad_column_format: %, expected booking.y{2026|2027}.qN', p_column; + END IF; + IF v_parts[2] NOT IN ('y2026','y2027') THEN + RAISE EXCEPTION 'bad_booking_year: % (only y2026/y2027)', v_parts[2]; + END IF; + IF v_parts[3] NOT IN ('q1','q2','q3','q4') THEN + RAISE EXCEPTION 'bad_booking_quarter: %', v_parts[3]; + END IF; + v_target_col := CASE v_parts[2] + WHEN 'y2026' THEN 'expenses_' || v_parts[3] + WHEN 'y2027' THEN 'expenses_next_year_' || v_parts[3] + END; + v_sql := format( + 'INSERT INTO v3.ckk (line_id, %1$I) VALUES ($1, ($2 #>> ''{}'')::NUMERIC) ' + || 'ON CONFLICT (line_id) DO UPDATE SET %1$I = EXCLUDED.%1$I', + v_target_col + ); + EXECUTE v_sql USING p_line_id, p_value; + UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id; + RETURN; + END IF; + + -- ── Маппинг (scope, field) → (table, col, type, key_kind, actor) ───── + -- key_kind: 'line' — PK/UQ = (line_id) + -- 'line_quarter'— PK = (line_id, quarter), quarter из v_q (берётся из scope qN) + -- 'line_actor' — UQ = (line_id, actor), actor из v_actor + -- + -- VALUES — это и есть white-list редактируемых полей. Поля, отсутствующие + -- здесь (computed: corrected_plan, new_plan, actual_quarter, rem_*, economy, + -- total, booking; FORM_2 q1.adj_rf — нет такого поля у FORM_2; и т.п.) → + -- падают с unknown_column ниже. + + -- Quarter scope qN → ставим v_q и нормализуем scope в 'q' для маппинга + IF v_scope IN ('q1','q2','q3','q4') THEN + v_q := substring(v_scope FROM 2)::SMALLINT; + ELSE + v_q := NULL; + END IF; + + -- Actor для sequestration scopes + v_actor := CASE v_scope + WHEN 'sequestration' THEN 'DFIP' + WHEN 'seq_dfip' THEN 'DFIP' + WHEN 'seq_ssp' THEN 'SSP_GO' + ELSE NULL END; + + SELECT m.target_table, m.target_col, m.target_type, m.key_kind + INTO v_target_table, v_target_col, v_target_type, v_key_kind + FROM (VALUES + -- ─── plan ────────────────────────────────────────────────────────── + ('plan','q1', 'plan','plan_q1','NUMERIC','line'), + ('plan','q2', 'plan','plan_q2','NUMERIC','line'), + ('plan','q3', 'plan','plan_q3','NUMERIC','line'), + ('plan','q4', 'plan','plan_q4','NUMERIC','line'), + ('plan','comment', 'plan','comment','TEXT', 'line'), + + -- ─── reserve ─────────────────────────────────────────────────────── + ('reserve','q1', 'reserve','amount_q1','NUMERIC','line'), + ('reserve','q2', 'reserve','amount_q2','NUMERIC','line'), + ('reserve','q3', 'reserve','amount_q3','NUMERIC','line'), + ('reserve','q4', 'reserve','amount_q4','NUMERIC','line'), + ('reserve','justification','reserve','justification','TEXT','line'), + + -- ─── allocation (FORM_1/4) ───────────────────────────────────────── + ('allocation','order', 'allocation','internal_order','TEXT','line'), + ('allocation','property', 'allocation','property_object','TEXT','line'), + ('allocation','contract_ref', 'allocation','contract_ref','TEXT','line'), + ('allocation','allocation_purpose', 'allocation','allocation_purpose','TEXT','line'), + + -- ─── contract_summary (FORM_1/4) ─────────────────────────────────── + ('contract_summary','total', 'contract_summary','total_amount', 'NUMERIC','line'), + ('contract_summary','counterparty', 'contract_summary','counterparty', 'TEXT', 'line'), + ('contract_summary','deadline', 'contract_summary','deadline', 'TEXT', 'line'), + ('contract_summary','comment', 'contract_summary','comment', 'TEXT', 'line'), + ('contract_summary','future_y1', 'contract_summary','future_payments_y1', 'NUMERIC','line'), + ('contract_summary','future_y2', 'contract_summary','future_payments_y2', 'NUMERIC','line'), + ('contract_summary','other_ssp', 'contract_summary','other_ssp_amount', 'NUMERIC','line'), + ('contract_summary','reference', 'contract_summary','reference', 'TEXT','line'), + ('contract_summary','centralized_flag', 'contract_summary','centralized_flag', 'TEXT','line'), + + -- ─── collegial_approval (FORM_1/4) ───────────────────────────────── + ('collegial','approved', 'collegial_approval','approved_amount', 'NUMERIC','line'), + ('collegial','protocol', 'collegial_approval','protocol_reference','TEXT', 'line'), + ('collegial','note', 'collegial_approval','note', 'TEXT', 'line'), + + -- ─── ckk (FORM_1/4) ──────────────────────────────────────────────── + ('ckk','ceiling', 'ckk','ceiling_amount', 'NUMERIC','line'), + ('ckk','q1', 'ckk','expenses_q1', 'NUMERIC','line'), + ('ckk','q2', 'ckk','expenses_q2', 'NUMERIC','line'), + ('ckk','q3', 'ckk','expenses_q3', 'NUMERIC','line'), + ('ckk','q4', 'ckk','expenses_q4', 'NUMERIC','line'), + ('ckk','next_q1', 'ckk','expenses_next_year_q1', 'NUMERIC','line'), + ('ckk','next_q2', 'ckk','expenses_next_year_q2', 'NUMERIC','line'), + ('ckk','next_q3', 'ckk','expenses_next_year_q3', 'NUMERIC','line'), + ('ckk','next_q4', 'ckk','expenses_next_year_q4', 'NUMERIC','line'), + ('ckk','rf_schedule', 'ckk','rf_schedule', 'TEXT', 'line'), + ('ckk','deadline', 'ckk','delivery_deadline', 'TEXT', 'line'), + ('ckk','proc_plan', 'ckk','procurement_plan', 'TEXT', 'line'), + ('ckk','proc_method', 'ckk','procurement_method','TEXT', 'line'), + ('ckk','comment', 'ckk','comment', 'TEXT', 'line'), + + -- ─── contract_detail (FORM_1) и его alias contract (FORM_2/4) ───── + ('contract_detail','counterparty', 'contract_detail','counterparty', 'TEXT', 'line'), + ('contract_detail','reference', 'contract_detail','reference', 'TEXT', 'line'), + ('contract_detail','addenda', 'contract_detail','addenda', 'TEXT', 'line'), + ('contract_detail','subject', 'contract_detail','subject', 'TEXT', 'line'), + ('contract_detail','currency', 'contract_detail','currency', 'TEXT', 'line'), + ('contract_detail','ceiling', 'contract_detail','ceiling_amount','NUMERIC','line'), + ('contract_detail','q1', 'contract_detail','expenses_q1', 'NUMERIC','line'), + ('contract_detail','q2', 'contract_detail','expenses_q2', 'NUMERIC','line'), + ('contract_detail','q3', 'contract_detail','expenses_q3', 'NUMERIC','line'), + ('contract_detail','q4', 'contract_detail','expenses_q4', 'NUMERIC','line'), + ('contract_detail','rf_schedule', 'contract_detail','rf_schedule', 'TEXT', 'line'), + ('contract_detail','vat_rate', 'contract_detail','vat_rate', 'TEXT', 'line'), + ('contract_detail','exchange_rate', 'contract_detail','exchange_rate', 'NUMERIC','line'), + ('contract_detail','amount_foreign', 'contract_detail','amount_foreign','NUMERIC','line'), + ('contract_detail','deadline', 'contract_detail','deadline', 'TEXT', 'line'), + ('contract_detail','payment_scheme', 'contract_detail','payment_scheme','TEXT', 'line'), + ('contract_detail','act', 'contract_detail','act', 'TEXT', 'line'), + ('contract_detail','comment', 'contract_detail','comment', 'TEXT', 'line'), + ('contract_detail','contract_date', 'contract_detail','contract_date', 'DATE', 'line'), + + ('contract','counterparty', 'contract_detail','counterparty', 'TEXT', 'line'), + ('contract','addenda', 'contract_detail','addenda', 'TEXT', 'line'), + ('contract','amount_foreign', 'contract_detail','amount_foreign','NUMERIC','line'), + ('contract','q1', 'contract_detail','expenses_q1', 'NUMERIC','line'), + ('contract','q2', 'contract_detail','expenses_q2', 'NUMERIC','line'), + ('contract','q3', 'contract_detail','expenses_q3', 'NUMERIC','line'), + ('contract','q4', 'contract_detail','expenses_q4', 'NUMERIC','line'), + ('contract','rf_schedule', 'contract_detail','rf_schedule', 'TEXT', 'line'), + ('contract','exchange_rate', 'contract_detail','exchange_rate', 'NUMERIC','line'), + ('contract','reference', 'contract_detail','reference', 'TEXT', 'line'), + ('contract','date', 'contract_detail','contract_date', 'DATE', 'line'), + ('contract','subject', 'contract_detail','subject', 'TEXT', 'line'), + ('contract','currency', 'contract_detail','currency', 'TEXT', 'line'), + ('contract','ceiling', 'contract_detail','ceiling_amount','NUMERIC','line'), + ('contract','vat_rate', 'contract_detail','vat_rate', 'TEXT', 'line'), + ('contract','deadline', 'contract_detail','deadline', 'TEXT', 'line'), + ('contract','scheme', 'contract_detail','payment_scheme','TEXT', 'line'), + ('contract','act', 'contract_detail','act', 'TEXT', 'line'), + ('contract','comment', 'contract_detail','comment', 'TEXT', 'line'), + + -- ─── sequestration (line_id, actor) — три алиаса scope ──────────── + ('sequestration','q1', 'sequestration','adj_q1', 'NUMERIC','line_actor'), + ('sequestration','q2', 'sequestration','adj_q2', 'NUMERIC','line_actor'), + ('sequestration','q3', 'sequestration','adj_q3', 'NUMERIC','line_actor'), + ('sequestration','q4', 'sequestration','adj_q4', 'NUMERIC','line_actor'), + ('sequestration','justification','sequestration','justification','TEXT', 'line_actor'), + + ('seq_dfip','q1', 'sequestration','adj_q1', 'NUMERIC','line_actor'), + ('seq_dfip','q2', 'sequestration','adj_q2', 'NUMERIC','line_actor'), + ('seq_dfip','q3', 'sequestration','adj_q3', 'NUMERIC','line_actor'), + ('seq_dfip','q4', 'sequestration','adj_q4', 'NUMERIC','line_actor'), + ('seq_dfip','justification','sequestration','justification','TEXT', 'line_actor'), + + ('seq_ssp','q1', 'sequestration','adj_q1', 'NUMERIC','line_actor'), + ('seq_ssp','q2', 'sequestration','adj_q2', 'NUMERIC','line_actor'), + ('seq_ssp','q3', 'sequestration','adj_q3', 'NUMERIC','line_actor'), + ('seq_ssp','q4', 'sequestration','adj_q4', 'NUMERIC','line_actor'), + ('seq_ssp','justification','sequestration','justification','TEXT', 'line_actor'), + + -- ─── budget_line_quarter — все 4 квартала через нормализованный scope='q' ─ + -- (v_scope qN, v_q заполнен; маппинг ниже использует scope='q') + ('q','adj_current', 'budget_line_quarter','adj_current', 'NUMERIC','line_quarter'), + ('q','adj_ssp', 'budget_line_quarter','adj_ssp', 'NUMERIC','line_quarter'), + ('q','adj_rf', 'budget_line_quarter','adj_rf', 'NUMERIC','line_quarter'), + ('q','adj_reserve', 'budget_line_quarter','adj_reserve', 'NUMERIC','line_quarter'), + ('q','adj_comment', 'budget_line_quarter','adj_comment', 'TEXT', 'line_quarter'), + ('q','target_change', 'budget_line_quarter','target_change', 'NUMERIC','line_quarter'), + ('q','base_correction', 'budget_line_quarter','base_plan_correction', 'NUMERIC','line_quarter'), + ('q','base_correction_comment', 'budget_line_quarter','base_plan_correction_comment','TEXT', 'line_quarter'), + ('q','pay_date', 'budget_line_quarter','payment_date', 'DATE', 'line_quarter'), + ('q','pay_amount', 'budget_line_quarter','payment_amount', 'NUMERIC','line_quarter'), + ('q','pay_ho', 'budget_line_quarter','payment_amount_ho', 'NUMERIC','line_quarter'), + ('q','pay_rf', 'budget_line_quarter','payment_amount_rf', 'NUMERIC','line_quarter'), + ('q','pay_comment', 'budget_line_quarter','payment_comment', 'TEXT', 'line_quarter'), + ('q','pay_act', 'budget_line_quarter','payment_act', 'TEXT', 'line_quarter'), + ('q','actual_m1', 'budget_line_quarter','actual_m1', 'NUMERIC','line_quarter'), + ('q','actual_m2', 'budget_line_quarter','actual_m2', 'NUMERIC','line_quarter'), + ('q','actual_m3', 'budget_line_quarter','actual_m3', 'NUMERIC','line_quarter'), + ('q','actual_spod', 'budget_line_quarter','actual_spod', 'NUMERIC','line_quarter'), + ('q','transfer_q2', 'budget_line_quarter','transfer_to_q2', 'NUMERIC','line_quarter'), + ('q','transfer_q3', 'budget_line_quarter','transfer_to_q3', 'NUMERIC','line_quarter'), + ('q','transfer_q4', 'budget_line_quarter','transfer_to_q4', 'NUMERIC','line_quarter'), + ('q','transfer_econ', 'budget_line_quarter','transfer_to_economy', 'NUMERIC','line_quarter'), + -- разбивка переноса в следующий квартал (transfer_q{N+1}_*) — единые поля, + -- индекс N+1 в имени ключа для UI смыслово важен, в БД одно и то же поле: + ('q','transfer_q2_delay_acts', 'budget_line_quarter','transfer_delay_acts', 'NUMERIC','line_quarter'), + ('q','transfer_q2_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'), + ('q','transfer_q2_economy_rf', 'budget_line_quarter','transfer_economy_rf', 'NUMERIC','line_quarter'), + ('q','transfer_q3_delay_acts', 'budget_line_quarter','transfer_delay_acts', 'NUMERIC','line_quarter'), + ('q','transfer_q3_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'), + ('q','transfer_q3_economy_rf', 'budget_line_quarter','transfer_economy_rf', 'NUMERIC','line_quarter'), + ('q','transfer_q4_delay_acts', 'budget_line_quarter','transfer_delay_acts', 'NUMERIC','line_quarter'), + ('q','transfer_q4_delay_procurement', 'budget_line_quarter','transfer_delay_procurement', 'NUMERIC','line_quarter'), + ('q','transfer_q4_economy_rf', 'budget_line_quarter','transfer_economy_rf', 'NUMERIC','line_quarter'), + ('q','transfer_next_comment', 'budget_line_quarter','transfer_next_comment', 'TEXT', 'line_quarter'), + ('q','transfer_far_comment', 'budget_line_quarter','transfer_far_comment', 'TEXT', 'line_quarter'), + -- plan_revision_* — две группы алиасов: FORM_1 (rev_*) и FORM_2/4 (revision_*). + ('q','rev_eco', 'budget_line_quarter','plan_revision_eco_change','NUMERIC','line_quarter'), + ('q','rev_item', 'budget_line_quarter','plan_revision_item_adj', 'NUMERIC','line_quarter'), + ('q','rev_inc', 'budget_line_quarter','plan_revision_increase', 'NUMERIC','line_quarter'), + ('q','rev_seq', 'budget_line_quarter','plan_revision_sequester', 'NUMERIC','line_quarter'), + ('q','rev_comment', 'budget_line_quarter','plan_revision_comment', 'TEXT', 'line_quarter'), + ('q','revision_inc', 'budget_line_quarter','plan_revision_increase', 'NUMERIC','line_quarter'), + ('q','revision_seq', 'budget_line_quarter','plan_revision_sequester', 'NUMERIC','line_quarter'), + ('q','revision_comment', 'budget_line_quarter','plan_revision_comment', 'TEXT', 'line_quarter'), + ('q','booking_amount', 'budget_line_quarter','booking_amount', 'NUMERIC','line_quarter') + ) AS m(scope, field, target_table, target_col, target_type, key_kind) + WHERE m.scope = (CASE WHEN v_q IS NOT NULL THEN 'q' ELSE v_scope END) + AND m.field = v_field; + + IF v_target_table IS NULL THEN + -- Распознаваемые computed-ключи отдельно — для понятной ошибки: + IF v_field IN ('corrected_plan','new_plan','actual_quarter','booking', + 'rem_booking','rem_actual','residual_after_booking', + 'residual_after_actual','economy','total','year') THEN + RAISE EXCEPTION 'computed_field: %', p_column; + END IF; + RAISE EXCEPTION 'unknown_column: %', p_column; + END IF; + + -- Извлекаем скаляр из JSONB — '#>> {}' возвращает NULL для jsonb null + -- Cast прицельный (NUMERIC/TEXT/DATE/INT) делается в dynamic SQL ниже. + + -- ── Dynamic UPSERT ──────────────────────────────────────────────────── + IF v_key_kind = 'line' THEN + v_sql := format( + 'INSERT INTO v3.%1$I (line_id, %2$I) VALUES ($1, ($2 #>> ''{}'')::%3$s) ' + || 'ON CONFLICT (line_id) DO UPDATE SET %2$I = EXCLUDED.%2$I', + v_target_table, v_target_col, v_target_type + ); + EXECUTE v_sql USING p_line_id, p_value; + + ELSIF v_key_kind = 'line_quarter' THEN + IF v_q IS NULL THEN RAISE EXCEPTION 'internal: quarter not set for %', p_column; END IF; + v_sql := format( + 'INSERT INTO v3.%1$I (line_id, quarter, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) ' + || 'ON CONFLICT (line_id, quarter) DO UPDATE SET %2$I = EXCLUDED.%2$I', + v_target_table, v_target_col, v_target_type + ); + EXECUTE v_sql USING p_line_id, v_q, p_value; + + ELSIF v_key_kind = 'line_actor' THEN + IF v_actor IS NULL THEN RAISE EXCEPTION 'internal: actor not set for %', p_column; END IF; + -- Для quarter-полей sequestration: scope.field=qN→adj_qN, justification без quarter + v_sql := format( + 'INSERT INTO v3.%1$I (line_id, actor, %2$I) VALUES ($1, $2, ($3 #>> ''{}'')::%3$s) ' + || 'ON CONFLICT (line_id, actor) DO UPDATE SET %2$I = EXCLUDED.%2$I', + v_target_table, v_target_col, v_target_type + ); + EXECUTE v_sql USING p_line_id, v_actor, p_value; + + ELSE + RAISE EXCEPTION 'internal: unknown key_kind %', v_key_kind; + END IF; + + -- Помечаем budget_line как изменённую (для updated_at iteration) + UPDATE v3.budget_line SET updated_at = now() WHERE id = p_line_id; +END; +$function$ +; + + +-- Удаляем FK у audit_log на случай удаления исконной записи и сохранения в журнале +ALTER TABLE v3.audit_log DROP CONSTRAINT IF EXISTS audit_log_app_user_fk; +ALTER TABLE v3.audit_log DROP CONSTRAINT IF EXISTS audit_log_org_unit_fk; +ALTER TABLE v3.audit_log DROP CONSTRAINT IF EXISTS audit_log_budget_form_fk; +ALTER TABLE v3.project ADD COLUMN created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(); + + +-- ════════════════════════════════════════════════════════════════════════════ +-- ПАТЧ: v3.v_form_view — добавление p_user_id + META-строки (editable-маска) +-- ════════════════════════════════════════════════════════════════════════════ +-- Идемпотентен. Пересоздаёт функцию (DROP обеих сигнатур + CREATE). +-- Зависимости (должны уже быть на целевой БД): v3.editable_columns_for(INT,INT,VARCHAR), +-- v3.user_role_code(INT), v3.budget_form, а также v_form1/2/4-функции. +-- +-- Применение: +-- psql -U dfip -d dfip -f patch_v_form_view_meta.sql +-- docker exec -i psql -U dfip -d dfip < patch_v_form_view_meta.sql +-- ════════════════════════════════════════════════════════════════════════════ + +-- ─── precheck зависимостей: падаем ДО DROP, если чего-то нет ───────────────── +DO $precheck$ +BEGIN + IF to_regprocedure('v3.editable_columns_for(int,int,varchar)') IS NULL THEN + RAISE EXCEPTION 'Зависимость отсутствует: v3.editable_columns_for(INT,INT,VARCHAR). Накатите sql/v3/permissions.sql'; + END IF; + IF to_regprocedure('v3.user_role_code(int)') IS NULL THEN + RAISE EXCEPTION 'Зависимость отсутствует: v3.user_role_code(INT). Накатите sql/v3/permissions.sql'; + END IF; +END +$precheck$; + +DROP FUNCTION IF EXISTS v3.v_form_view(INT, VARCHAR, TEXT[], VARCHAR); +DROP FUNCTION IF EXISTS v3.v_form_view(INT, VARCHAR, TEXT[], VARCHAR, INT); + +CREATE FUNCTION v3.v_form_view( + p_form_id INT, + p_sheet VARCHAR, + p_sections TEXT[] DEFAULT NULL, + p_direction VARCHAR DEFAULT NULL, + p_user_id INT DEFAULT NULL +) +RETURNS TABLE ( + row_type VARCHAR, + depth INT, + sort_order BIGINT, + data JSONB +) +AS $function$ +DECLARE + v_form_type VARCHAR; + v_year INT; + v_org_unit_id INT; +BEGIN + -- Определяем form_type автоматически + SELECT form_type_code, year, org_unit_id + INTO v_form_type, v_year, v_org_unit_id + FROM v3.budget_form + WHERE id = p_form_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'budget_form id=% не существует в v3.budget_form', p_form_id; + END IF; + + -- ═══ META-строка (маска редактируемых колонок) ════════════════════════════ + -- Отдаётся первой (sort_order=0), только если передан p_user_id. НЕ гейт на + -- просмотр — сами строки листа возвращаются ниже независимо от прав. + IF p_user_id IS NOT NULL THEN + RETURN QUERY + SELECT + 'META'::VARCHAR, + -1, + 0::BIGINT, + jsonb_build_object( + 'user_id', p_user_id, + 'role', v3.user_role_code(p_user_id), + 'sheet', p_sheet, + 'editable', COALESCE( + (SELECT jsonb_agg( + jsonb_build_object('column', ec.column_key, + 'closes_at', ec.closes_at) + ORDER BY ec.column_key) + FROM v3.editable_columns_for(p_form_id, p_user_id, p_sheet) ec), + '[]'::JSONB) + ); + END IF; + + -- ═══ FORM_1 ═══════════════════════════════════════════════════════════════ + IF v_form_type = 'FORM_1' THEN + + IF p_sheet = 'SMETA' THEN + -- v3.v_form1_smeta параметризуется year+org_unit_id + RETURN QUERY + SELECT + sm.row_type, + sm.depth, + ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, + jsonb_build_object( + 'section_code', sm.section_code, + 'name', sm.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_plan_q1, 'q2', sm.supp_plan_q2, + 'q3', sm.supp_plan_q3, 'q4', sm.supp_plan_q4, + 'year', sm.supp_plan_year), + 'development', jsonb_build_object('q1', sm.dev_plan_q1, 'q2', sm.dev_plan_q2, + 'q3', sm.dev_plan_q3, 'q4', sm.dev_plan_q4, + 'year', sm.dev_plan_year), + 'total_year', sm.total_plan_year), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_appr_q1, 'q2', sm.supp_appr_q2, + 'q3', sm.supp_appr_q3, 'q4', sm.supp_appr_q4, + 'year', sm.supp_appr_year), + 'development', jsonb_build_object('q1', sm.dev_appr_q1, 'q2', sm.dev_appr_q2, + 'q3', sm.dev_appr_q3, 'q4', sm.dev_appr_q4, + 'year', sm.dev_appr_year), + 'total_year', sm.total_appr_year), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_act_q1, 'q2', sm.supp_act_q2, + 'q3', sm.supp_act_q3, 'q4', sm.supp_act_q4, + 'year', sm.supp_act_year), + 'development', jsonb_build_object('q1', sm.dev_act_q1, 'q2', sm.dev_act_q2, + 'q3', sm.dev_act_q3, 'q4', sm.dev_act_q4, + 'year', sm.dev_act_year), + 'total_year', sm.total_act_year), + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', sm.supp_corr_q2, 'q3', sm.supp_corr_q3, 'q4', sm.supp_corr_q4), + 'development', jsonb_build_object('q2', sm.dev_corr_q2, 'q3', sm.dev_corr_q3, 'q4', sm.dev_corr_q4)) + ) + FROM v3.v_form1_smeta(v_year, v_org_unit_id) sm + ORDER BY sm.section_code; + RETURN; + END IF; + + IF p_sheet IN ('AHR','CAP','OPER') THEN + RETURN QUERY + SELECT + j.row_type, + j.depth, + ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order, + jsonb_build_object( + 'line_id', j.line_id, + 'header', j.header, + 'plan', j.plan_data, + 'contract_summary', j.contract_summary, + 'allocation', j.allocation_data, + 'sequestration', j.sequestration_data, + 'reserve', j.reserve_data, + 'approved', j.approved_data, + 'collegial', j.collegial_data, + 'ckk', j.ckk_data, + 'contract_detail', j.contract_detail, + 'q1', j.q1_data, + 'q2', j.q2_data, + 'q3', j.q3_data, + 'q4', j.q4_data, + 'totals', j.totals_data + ) + FROM v3.v_form1_sheet_jsonb(p_form_id, p_sheet, p_direction, p_sections) j + ORDER BY j._sort_path; + RETURN; + END IF; + + RAISE EXCEPTION 'Unknown FORM_1 sheet: %', p_sheet + USING HINT = 'Use AHR / CAP / OPER / SMETA'; + END IF; + + -- ═══ FORM_2 ═══════════════════════════════════════════════════════════════ + IF v_form_type = 'FORM_2' THEN + RETURN QUERY SELECT * FROM v3.v_form2_view(p_form_id, p_sheet, p_sections); + RETURN; + END IF; + + -- ═══ FORM_4 ═══════════════════════════════════════════════════════════════ + IF v_form_type = 'FORM_4' THEN + RETURN QUERY SELECT * FROM v3.v_form4_view(p_form_id, p_sheet, p_sections); + RETURN; + END IF; + + RAISE EXCEPTION 'Unsupported form_type=% for form_id=%', v_form_type, p_form_id; + +END; +$function$ +LANGUAGE plpgsql STABLE; + + +-- ════════════════════════════════════════════════════════════════════════════ +-- ПАТЧ: v3.v_form3_report_jsonb — добавление p_user_id + META-строки (editable) +-- ════════════════════════════════════════════════════════════════════════════ +-- Идемпотентен. Пересоздаёт функцию (DROP 2-арг и 3-арг сигнатур + CREATE). +-- Зависимости (должны быть на целевой БД): v3.editable_columns_for3(INT,INT), +-- v3.user_role_code(INT), v3.rf_project_report, v3.v_form3_report_sections. +-- +-- Применение: +-- psql -U dfip -d dfip -f patch_v_form3_report_jsonb_meta.sql +-- docker exec -i psql -U dfip -d dfip < patch_v_form3_report_jsonb_meta.sql +-- ════════════════════════════════════════════════════════════════════════════ + +DO $precheck$ +BEGIN + IF to_regprocedure('v3.editable_columns_for3(int,int)') IS NULL THEN + RAISE EXCEPTION 'Зависимость отсутствует: v3.editable_columns_for3(INT,INT). Накатите sql/v3/permissions_form3.sql'; + END IF; + IF to_regprocedure('v3.user_role_code(int)') IS NULL THEN + RAISE EXCEPTION 'Зависимость отсутствует: v3.user_role_code(INT). Накатите sql/v3/permissions.sql'; + END IF; +END +$precheck$; + +-- ════════════════════════════════════════════════════════════════════════════ +-- v3.v_form3_report_jsonb — JSONB-обёртка над v_form3_report_sections +-- ════════════════════════════════════════════════════════════════════════════ +-- Один трек FORM_3 (LIMIT либо CURRENT_EXPENSES) как (row_type, depth, +-- sort_order, data jsonb) — общий формат UI с FORM_1/2/4. +-- +-- p_user_id — если задан, ПЕРВОЙ строкой (row_type='META', depth=-1, +-- sort_order=0) идёт маска редактируемых колонок текущей фазы для этого юзера: +-- data = { user_id, role, report_id, report_type, editable:[{column,closes_at}] }. +-- Маска — v3.editable_columns_for3(report, user) (якорь rf_project_report, +-- лист/трек определяется отчётом внутри). Это НЕ гейт на просмотр — строки +-- отчёта отдаются всегда; при отсутствии прав editable=[] → фронт read-only. +-- NULL (по умолчанию) → META-строки нет (обратная совместимость). +-- ════════════════════════════════════════════════════════════════════════════ + +DROP FUNCTION IF EXISTS v3.v_form3_report_jsonb(INT, TEXT[]); +DROP FUNCTION IF EXISTS v3.v_form3_report_jsonb(INT, TEXT[], INT); + +CREATE FUNCTION v3.v_form3_report_jsonb( + p_report_id INT, + p_sections TEXT[] DEFAULT NULL, + p_user_id INT DEFAULT NULL +) +RETURNS TABLE ( + row_type VARCHAR, + depth INT, + sort_order BIGINT, + data JSONB +) +AS $function$ + SELECT u.row_type, u.depth, u.sort_order, u.data + FROM ( + -- ═══ META-строка (маска редактируемых колонок) ══════════════════════════ + -- Только при переданном p_user_id. НЕ гейт на просмотр. + SELECT + 'META'::VARCHAR AS row_type, + -1 AS depth, + 0::BIGINT AS sort_order, + jsonb_build_object( + 'user_id', p_user_id, + 'role', v3.user_role_code(p_user_id), + 'report_id', p_report_id, + 'report_type', (SELECT report_type FROM v3.rf_project_report WHERE id = p_report_id), + 'editable', COALESCE( + (SELECT jsonb_agg( + jsonb_build_object('column', ec.column_key, + 'closes_at', ec.closes_at) + ORDER BY ec.column_key) + FROM v3.editable_columns_for3(p_report_id, p_user_id) ec), + '[]'::JSONB) + ) AS data + WHERE p_user_id IS NOT NULL + + UNION ALL + + SELECT + s.row_type, + s.depth, + ROW_NUMBER() OVER (ORDER BY s._sort_path) AS sort_order, + jsonb_build_object( + 'line_id', s.line_id, + 'header', jsonb_build_object( + 'section_code', s.col_section_code, + 'item_id', s.col_item_id, + 'num_group_id', s.col_num_group_id, + 'name', s.col_name, + -- expense_item_id из _sort_path: для INPUT путь = {...ei_id, line_id}, + -- для иерархии = {...ei_id} → последний/предпоследний элемент. + 'expense_item_id', CASE WHEN s.row_type = 'INPUT' + THEN s._sort_path[array_upper(s._sort_path,1)-1] + ELSE s._sort_path[array_upper(s._sort_path,1)] END + ), + 'q1', jsonb_build_object( + 'base_plan', s.col_q1_base_plan, + 'adj_by_items', s.col_q1_adj_by_items, + 'adj_increase', s.col_q1_adj_increase, + 'total_corr', s.col_q1_total_corr, + 'm1', s.col_q1_m1, + 'm2', s.col_q1_m2, + 'm3', s.col_q1_m3, + 'quarter_actual', s.col_q1_quarter_actual, + 'economy', s.col_q1_economy -- «Остаток» = корр+увелич−квартал + ), + 'q2', jsonb_build_object( + 'base_plan', s.col_q2_base_plan, + 'corrected_plan', s.col_q2_corrected_plan, + 'carryover', s.col_q2_carryover, + 'adj_by_items', s.col_q2_adj_by_items, + 'adj_increase', s.col_q2_adj_increase, + 'total_corr', s.col_q2_total_corr, + 'm1', s.col_q2_m1, + 'm2', s.col_q2_m2, + 'm3', s.col_q2_m3, + 'quarter_actual', s.col_q2_quarter_actual, + 'economy', s.col_q2_economy + ), + 'q3', jsonb_build_object( + 'base_plan', s.col_q3_base_plan, + 'corrected_plan', s.col_q3_corrected_plan, + 'carryover', s.col_q3_carryover, + 'adj_by_items', s.col_q3_adj_by_items, + 'adj_increase', s.col_q3_adj_increase, + 'total_corr', s.col_q3_total_corr, + 'm1', s.col_q3_m1, + 'm2', s.col_q3_m2, + 'm3', s.col_q3_m3, + 'quarter_actual', s.col_q3_quarter_actual, + 'economy', s.col_q3_economy + ), + 'q4', jsonb_build_object( + 'base_plan', s.col_q4_base_plan, + 'corrected_plan', s.col_q4_corrected_plan, + 'carryover', s.col_q4_carryover, + 'adj_by_items', s.col_q4_adj_by_items, + 'adj_increase', s.col_q4_adj_increase, + 'total_corr', s.col_q4_total_corr, + 'm1', s.col_q4_m1, + 'm2', s.col_q4_m2, + 'm3', s.col_q4_m3, + 'spod', s.col_q4_spod, + 'quarter_actual', s.col_q4_quarter_actual, + 'economy', s.col_q4_economy + ), + 'totals', jsonb_build_object( + 'base_plan', s.col_year_base_plan, -- Год Базового плана (Σ q1–q4) + 'total_corr', s.col_year_total_corr, + 'total_actual', s.col_year_total_actual, + 'economy', s.col_year_economy + ) + ) AS data + FROM v3.v_form3_report_sections(p_report_id, p_sections) s + ) u + ORDER BY u.sort_order; +$function$ +LANGUAGE sql STABLE; + diff --git a/api/src/api/v1/projects.py b/api/src/api/v1/projects.py index b5291c4..08a57ca 100644 --- a/api/src/api/v1/projects.py +++ b/api/src/api/v1/projects.py @@ -15,6 +15,7 @@ from src.domain.schemas import ( BaseSingleResponse, CellPatch, CellsPatch, + ResponseBase, SheetResponse, UpdProjectBody, ) @@ -251,6 +252,17 @@ async def upd_project( return result +@router.delete("/projects/{project_id}") +async def delete_project( + project_id: int, + db: AsyncSession = Depends(get_db), + current_user: AppUser = Depends(get_current_active_user_with_set_db), +) -> ResponseBase: + project_service = ProjectService(db) + await project_service.delete_project(project_id=project_id, user=current_user) + return ResponseBase(success=True, message="Проект удалён") + + @router.post("/projects") async def add_project( body: AddProjectBody, diff --git a/api/src/api/v1/websocket.py b/api/src/api/v1/websocket.py index a4c16c8..9768e9f 100644 --- a/api/src/api/v1/websocket.py +++ b/api/src/api/v1/websocket.py @@ -707,10 +707,10 @@ async def login(websocket: WebSocket, **kwargs) -> int: def resolve_cell_key(event_data: dict) -> dict | None: data = event_data.get("data") - if not data or "line_id" not in data or "column" not in data: + if not data or ("line_id" not in data and "line_id_code" not in data) or "column" not in data: return None return { - "line_id": data.get("line_id_code", data["line_id"]), + "line_id": data.get("line_id_code") or data.get("line_id"), "column": data["column"], } diff --git a/api/src/db/models/project.py b/api/src/db/models/project.py index cba2a1a..36785af 100644 --- a/api/src/db/models/project.py +++ b/api/src/db/models/project.py @@ -1,6 +1,8 @@ from __future__ import annotations -from sqlalchemy import CheckConstraint, ForeignKey, Index, Integer, Numeric, String, text +from datetime import datetime + +from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, Numeric, String, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship from src.db.base import Base @@ -41,6 +43,7 @@ class Project(Base): staff_count: Mapped[int | None] = mapped_column(Integer) total_area: Mapped[float | None] = mapped_column(Numeric) org_unit_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.org_unit.id")) + created_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now()) # parent = relationship("Project", remote_side="Project.id", back_populates="children") # children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore diff --git a/api/src/repository/project_repository.py b/api/src/repository/project_repository.py index d22c7c0..4703fcc 100644 --- a/api/src/repository/project_repository.py +++ b/api/src/repository/project_repository.py @@ -28,6 +28,7 @@ class ProjectRepository: "total_area": float(project.total_area) if project.total_area is not None else None, "org_unit_id": project.org_unit_id, "org_unit_name": org_unit_name, + "created_at": project.created_at.isoformat() if project.created_at else None, "report_count": int(report_count), "years": years, } @@ -316,6 +317,15 @@ class ProjectRepository: rows = (await self.db.execute(query, {"line_id": line_id})).all() return [tuple(r) for r in rows] + async def del_project(self, project_id: int): + query = text( + """ + SELECT v3.del_project(CAST(:project_id AS INT)) + """ + ) + result = (await self.db.execute(query, {"project_id": project_id})).scalar_one() + return result + async def upd_project(self, project_id: int, column: str, value): query = text( """ diff --git a/api/src/services/project_service.py b/api/src/services/project_service.py index 170ac99..bbf6455 100644 --- a/api/src/services/project_service.py +++ b/api/src/services/project_service.py @@ -147,6 +147,12 @@ class ProjectService: raise ValidationException("Отчёт не найден") return await self.project_repo.del_form3_line(line_id) + async def delete_project(self, project_id: int, user: AppUser): + project = await self.get(project_id, user) + if not project: + raise ValidationException("Проект не найден") + return await self.project_repo.del_project(project_id) + async def upd_project( self, project_id: int, diff --git a/api/src/services/user_service.py b/api/src/services/user_service.py index 55db948..009f2e7 100644 --- a/api/src/services/user_service.py +++ b/api/src/services/user_service.py @@ -85,6 +85,11 @@ class UserService: ) update_payload["role_id"] = role.value + if user.id == user_id and not update_payload.get('is_active', True): + raise ValidationException( + "Нельзя удалить себя", field="role_id" + ) + if "username" in update_payload and update_payload["username"] != target.username: conflict = await self.user_repo.get_by_username(update_payload["username"]) if conflict: @@ -122,6 +127,10 @@ class UserService: async def delete_user(self, user_id: int, user: AppUser): if user.role_id != UserRoleEnum.ADMIN: raise AccessDeniedException() + if user.id == user_id: + raise ValidationException( + "Нельзя удалить себя", field="role_id" + ) deleted = await self.user_repo.logical_delete(user_id) if not deleted: raise UserNotFoundException() diff --git a/api/tests/integration/test_projects_api_smoke.py b/api/tests/integration/test_projects_api_smoke.py index b480698..86e058d 100644 --- a/api/tests/integration/test_projects_api_smoke.py +++ b/api/tests/integration/test_projects_api_smoke.py @@ -1,3 +1,4 @@ +from datetime import datetime import uuid import pytest @@ -12,11 +13,11 @@ def test_projects_list_smoke(client, admin_tokens, auth_headers): assert isinstance(payload["result"], list) -def _create_project(client, admin_tokens, auth_headers) -> tuple[int, str]: +def _create_project(client, admin_tokens, auth_headers, year=2026, branch_id=1) -> tuple[int, str]: name = f"PT_{uuid.uuid4().hex[:10]}" response = client.post( "/api/v1/projects", - json={"name": name, "year": 2026, "branch_id": 1}, + json={"name": name, "year": year, "branch_id": branch_id}, headers=auth_headers(admin_tokens), ) assert response.status_code == 200 @@ -24,10 +25,10 @@ def _create_project(client, admin_tokens, auth_headers) -> tuple[int, str]: return payload["project_id"], name -def _add_line(client, admin_tokens, auth_headers, project_id: int) -> int: +def _add_line(client, admin_tokens, auth_headers, project_id: int, year: int = 2026) -> int: for expense_item_id in range(1, 80): response = client.post( - f"/api/v1/projects/{project_id}/report/2026/LIMIT/line", + f"/api/v1/projects/{project_id}/report/{year}/LIMIT/line", json={"expense_item_id": expense_item_id}, headers=auth_headers(admin_tokens), ) @@ -143,3 +144,107 @@ def test_projects_write_report_not_found_error_shape(client, admin_tokens, auth_ payload = response.json() assert isinstance(payload, dict) assert payload.get("message") == "Отчёт не найден" + + +def test_delete_project_smoke(client, admin_tokens, auth_headers): + project_id, _ = _create_project(client, admin_tokens, auth_headers, year=datetime.now().year) + + response = client.delete( + f"/api/v1/projects/{project_id}", + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 200 + payload = response.json() + assert payload["success"] is True + assert payload["message"] == "Проект удалён" + + +def test_delete_project_verify_gone(client, admin_tokens, auth_headers): + project_id, _ = _create_project(client, admin_tokens, auth_headers, year=datetime.now().year) + + del_response = client.delete( + f"/api/v1/projects/{project_id}", + headers=auth_headers(admin_tokens), + ) + assert del_response.status_code == 200 + + get_response = client.get( + f"/api/v1/projects/{project_id}", + headers=auth_headers(admin_tokens), + ) + assert get_response.status_code == 404 + + +def test_delete_project_not_found(client, admin_tokens, auth_headers): + response = client.delete( + "/api/v1/projects/99999", + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 400 + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload + + +def test_delete_project_unauthorized(client): + response = client.delete("/api/v1/projects/1") + assert response.status_code == 403 + +#ToDo: вернуть, когда пофиксим смещение фаз в зависимости от года +# def test_delete_project_with_lines_blocked(client, admin_tokens, auth_headers): +# """Проект со строками отчёта нельзя удалить: can_delete_project → false → RAISE EXCEPTION.""" +# year = datetime.now().year - 1 +# project_id, _ = _create_project(client, admin_tokens, auth_headers, year=year) +# _add_line(client, admin_tokens, auth_headers, project_id, year=year) + +# response = client.delete( +# f"/api/v1/projects/{project_id}", +# headers=auth_headers(admin_tokens), +# ) +# assert response.status_code == 400 +# payload = response.json() +# assert isinstance(payload, dict) +# assert "code" in payload +# assert "message" in payload +# assert "невозможно удалить" in payload["message"] + + +def test_delete_project_executor_from_other_org(client, admin_tokens, auth_headers, isp_tokens): + """ISP1 (привязан к РФ id=2) не может удалить проект на ССП id=1 — ACL.""" + project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=1, year=datetime.now().year) + + response = client.delete( + f"/api/v1/projects/{project_id}", + headers=auth_headers(isp_tokens), + ) + assert response.status_code == 400 + payload = response.json() + assert "message" in payload + assert payload["message"] == "Проект не найден" + + +#ToDo: вернуть, когда пофиксим смещение фаз в зависимости от года +# def test_delete_project_executor_from_same_org(client, isp_tokens, auth_headers): +# """ISP1 может удалить проект на своём РФ (id=2), если нет строк отчёта.""" +# project_id = _create_project(client, isp_tokens, auth_headers, branch_id=2, year=datetime.now().year) + +# response = client.delete( +# f"/api/v1/projects/{project_id}", +# headers=auth_headers(isp_tokens), +# ) +# assert response.status_code == 200 +# payload = response.json() +# assert payload["success"] is True +# assert payload["message"] == "Проект удалён" + + +def test_delete_seed_project_smoke(client, admin_tokens, auth_headers): + """Проект из фикстуры (id=2) удаляется: у него нет строк отчёта, can_delete_project → true.""" + response = client.delete( + "/api/v1/projects/2", + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 200 + payload = response.json() + assert payload["success"] is True + assert payload["message"] == "Проект удалён" diff --git a/web/src/components/RealtimeTable/constants/FORM_4/AHR.js b/web/src/components/RealtimeTable/constants/FORM_4/AHR.js index 3793ff6..8126a7a 100644 --- a/web/src/components/RealtimeTable/constants/FORM_4/AHR.js +++ b/web/src/components/RealtimeTable/constants/FORM_4/AHR.js @@ -46,9 +46,9 @@ "color_type":greenColumn, "accessorKey":"data.plan.comment" }, - "data.contract_summary.amount":{ + "data.contract_summary.total":{ "color_type":greenColumn, - "accessorKey":"data.contract_summary.amount" + "accessorKey":"data.contract_summary.total" }, "data.contract_summary.reference":{ "color_type":greenColumn, @@ -66,17 +66,17 @@ "color_type":greenColumn, "accessorKey":"data.contract_summary.comment" }, - "data.contract_summary.future_payments_y1":{ + "data.contract_summary.future_y1":{ "color_type":greenColumn, - "accessorKey":"data.contract_summary.future_payments_y1" + "accessorKey":"data.contract_summary.future_y1" }, - "data.contract_summary.future_payments_y2":{ + "data.contract_summary.future_y2":{ "color_type":greenColumn, - "accessorKey":"data.contract_summary.future_payments_y2" + "accessorKey":"data.contract_summary.future_y2" }, - "data.contract_summary.other_ssp_amount":{ + "data.contract_summary.other_ssp":{ "color_type":greenColumn, - "accessorKey":"data.contract_summary.other_ssp_amount" + "accessorKey":"data.contract_summary.other_ssp" }, "data.contract_summary.centralized_flag":{ "color_type":greenColumn, @@ -158,13 +158,13 @@ "color_type":greenColumn, "accessorKey":"data.approved.year" }, - "data.collegial.amount":{ + "data.collegial.approved":{ "color_type":orangeColumn, - "accessorKey":"data.collegial.amount" + "accessorKey":"data.collegial.approved" }, - "data.collegial.protocol_reference":{ + "data.collegial.protocol":{ "color_type":orangeColumn, - "accessorKey":"data.collegial.protocol_reference" + "accessorKey":"data.collegial.protocol" }, "field_kollegialnye_organy_field":{ "color_type":orangeColumn, @@ -182,37 +182,37 @@ "color_type":orangeColumn, "accessorKey":"data.ckk.ceiling" }, - "data.ckk.expenses_q1":{ + "data.ckk.q1":{ "color_type":orangeColumn, - "accessorKey":"data.ckk.expenses_q1" + "accessorKey":"data.ckk.q1" }, - "data.ckk.expenses_q2":{ + "data.ckk.q2":{ "color_type":orangeColumn, - "accessorKey":"data.ckk.expenses_q2" + "accessorKey":"data.ckk.q2" }, - "data.ckk.expenses_q3":{ + "data.ckk.q3":{ "color_type":orangeColumn, - "accessorKey":"data.ckk.expenses_q3" + "accessorKey":"data.ckk.q3" }, - "data.ckk.expenses_q4":{ + "data.ckk.q4":{ "color_type":orangeColumn, - "accessorKey":"data.ckk.expenses_q4" + "accessorKey":"data.ckk.q4" }, "data.ckk.rf_schedule":{ "color_type":orangeColumn, "accessorKey":"data.ckk.rf_schedule" }, - "data.ckk.delivery_deadline":{ + "data.ckk.deadline":{ "color_type":orangeColumn, - "accessorKey":"data.ckk.delivery_deadline" + "accessorKey":"data.ckk.deadline" }, - "data.ckk.procurement_plan":{ + "data.ckk.proc_plan":{ "color_type":orangeColumn, - "accessorKey":"data.ckk.procurement_plan" + "accessorKey":"data.ckk.proc_plan" }, - "data.ckk.procurement_method":{ + "data.ckk.proc_method":{ "color_type":orangeColumn, - "accessorKey":"data.ckk.procurement_method" + "accessorKey":"data.ckk.proc_method" }, "data.ckk.comment":{ "color_type":orangeColumn, @@ -242,21 +242,21 @@ "color_type":orangeColumn, "accessorKey":"data.contract.ceiling" }, - "data.contract.expenses_q1":{ + "data.contract.q1":{ "color_type":orangeColumn, - "accessorKey":"data.contract.expenses_q1" + "accessorKey":"data.contract.q1" }, - "data.contract.expenses_q2":{ + "data.contract.q2":{ "color_type":orangeColumn, - "accessorKey":"data.contract.expenses_q2" + "accessorKey":"data.contract.q2" }, - "data.contract.expenses_q3":{ + "data.contract.q3":{ "color_type":orangeColumn, - "accessorKey":"data.contract.expenses_q3" + "accessorKey":"data.contract.q3" }, - "data.contract.expenses_q4":{ + "data.contract.q4":{ "color_type":orangeColumn, - "accessorKey":"data.contract.expenses_q4" + "accessorKey":"data.contract.q4" }, "data.contract.rf_schedule":{ "color_type":orangeColumn, @@ -955,7 +955,7 @@ "columns":[ { "header":"Сумма", - "accessorKey":"data.contract_summary.amount", + "accessorKey":"data.contract_summary.total", "columnLetter":"L", "size":150, "filterFn":"contains", @@ -1020,7 +1020,7 @@ }, { "header":"Платежив 2027 году", - "accessorKey":"data.contract_summary.future_payments_y1", + "accessorKey":"data.contract_summary.future_y1", "columnLetter":"Q", "size":190, "filterFn":"contains", @@ -1033,7 +1033,7 @@ }, { "header":"Платежив 2028 году", - "accessorKey":"data.contract_summary.future_payments_y2", + "accessorKey":"data.contract_summary.future_y2", "columnLetter":"R", "size":190, "filterFn":"contains", @@ -1046,7 +1046,7 @@ }, { "header":"Сумма в других ССП", - "accessorKey":"data.contract_summary.other_ssp_amount", + "accessorKey":"data.contract_summary.other_ssp", "columnLetter":"S", "size":180, "filterFn":"contains", @@ -1475,7 +1475,7 @@ "columns":[ { "header":"Сумма (без НДС)", - "accessorKey":"data.collegial.amount", + "accessorKey":"data.collegial.approved", "columnLetter":"AS", "size":150, "filterFn":"contains", @@ -1488,7 +1488,7 @@ }, { "header":"Реквизиты протокола (дата, номер)", - "accessorKey":"data.collegial.protocol_reference", + "accessorKey":"data.collegial.protocol", "columnLetter":"AT", "size":330, "filterFn":"contains", @@ -1541,7 +1541,7 @@ }, { "header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.ckk.expenses_q1", + "accessorKey":"data.ckk.q1", "columnLetter":"AY", "size":420, "filterFn":"contains", @@ -1554,7 +1554,7 @@ }, { "header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.ckk.expenses_q2", + "accessorKey":"data.ckk.q2", "columnLetter":"AZ", "size":420, "filterFn":"contains", @@ -1567,7 +1567,7 @@ }, { "header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.ckk.expenses_q3", + "accessorKey":"data.ckk.q3", "columnLetter":"BA", "size":420, "filterFn":"contains", @@ -1580,7 +1580,7 @@ }, { "header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.ckk.expenses_q4", + "accessorKey":"data.ckk.q4", "columnLetter":"BB", "size":420, "filterFn":"contains", @@ -1606,7 +1606,7 @@ }, { "header":"Срок поставки", - "accessorKey":"data.ckk.delivery_deadline", + "accessorKey":"data.ckk.deadline", "columnLetter":"BD", "size":150, "filterFn":"contains", @@ -1619,7 +1619,7 @@ }, { "header":"План закупок", - "accessorKey":"data.ckk.procurement_plan", + "accessorKey":"data.ckk.proc_plan", "columnLetter":"BE", "size":150, "filterFn":"contains", @@ -1632,7 +1632,7 @@ }, { "header":"Способ закупки", - "accessorKey":"data.ckk.procurement_method", + "accessorKey":"data.ckk.proc_method", "columnLetter":"BF", "size":150, "filterFn":"contains", @@ -1748,7 +1748,7 @@ }, { "header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.contract.expenses_q1", + "accessorKey":"data.contract.q1", "columnLetter":"BO", "size":420, "filterFn":"contains", @@ -1761,7 +1761,7 @@ }, { "header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.contract.expenses_q2", + "accessorKey":"data.contract.q2", "columnLetter":"BP", "size":420, "filterFn":"contains", @@ -1774,7 +1774,7 @@ }, { "header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.contract.expenses_q3", + "accessorKey":"data.contract.q3", "columnLetter":"BQ", "size":420, "filterFn":"contains", @@ -1787,7 +1787,7 @@ }, { "header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)", - "accessorKey":"data.contract.expenses_q4", + "accessorKey":"data.contract.q4", "columnLetter":"BR", "size":420, "filterFn":"contains", diff --git a/web/src/pages/DictVspPage/DictVspPage.jsx b/web/src/pages/DictVspPage/DictVspPage.jsx index 27c7280..d8b425f 100644 --- a/web/src/pages/DictVspPage/DictVspPage.jsx +++ b/web/src/pages/DictVspPage/DictVspPage.jsx @@ -45,7 +45,7 @@ function DictVspPage() { const isAdmin = user.role_id == ROLES_NAME_ID.admin; const [vspResponse, sspResponse] = await Promise.all([ DictVspApi.get(), - isAdmin ? SspApi.getAll() : Promise.resolve(null), + isAdmin ? SspApi.getAll({is_active: true}) : Promise.resolve(null), ]); if (!vspResponse.success) {