From 370e2e87febe84de7a388bccda2564fdf7af9be7 Mon Sep 17 00:00:00 2001 From: tsygankoviva Date: Tue, 22 Sep 2026 18:24:41 +0300 Subject: [PATCH] =?UTF-8?q?form1-sub=5Fsub-fix:=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D0=B2=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=B8=D0=B7=20=D1=81=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../versions/sql/0034_ahr_rf_sort_order.sql | 786 +++++++++++++++++- 1 file changed, 768 insertions(+), 18 deletions(-) diff --git a/api/alembic/versions/sql/0034_ahr_rf_sort_order.sql b/api/alembic/versions/sql/0034_ahr_rf_sort_order.sql index 6a203fa..e22dbc1 100644 --- a/api/alembic/versions/sql/0034_ahr_rf_sort_order.sql +++ b/api/alembic/versions/sql/0034_ahr_rf_sort_order.sql @@ -1,10 +1,3 @@ --- Fix sort.docx items 1-2: --- - AHR RF depreciation and 5.00.0 tax block order. --- - FORM_3 R061012024 order between R061012023 and R061012121. --- - FORM_4 SMETA excludes sections absent from the project estimate template. --- v3.mv_expense_item_tree sorts by expense_item.id path, so the catalog ids --- must be renumbered together with all foreign-key references. - ALTER TABLE v3.budget_line DROP CONSTRAINT IF EXISTS budget_line_expense_item_fk; ALTER TABLE v3.expense_item DROP CONSTRAINT IF EXISTS expense_item_expense_item_fk; ALTER TABLE v3.expense_item_form_type DROP CONSTRAINT IF EXISTS expense_item_form_type_expense_item_fk; @@ -155,22 +148,779 @@ WHERE form_type_code = 'FORM_4' 800, 801, 802, 803, 804, 805, 806, 807, 808 ); -DO $block$ +CREATE OR REPLACE FUNCTION v3.sync_form2_ahr_satellite_lines(p_form_id integer) + RETURNS void + LANGUAGE plpgsql +AS $function$ DECLARE - v_function_definition TEXT; + v_form_type varchar; + v_line_id integer; + v_rec record; BEGIN - SELECT pg_get_functiondef('v3.v_summary_view(integer, character varying, integer[])'::regprocedure) - INTO v_function_definition; + SELECT bf.form_type_code + INTO v_form_type + FROM v3.budget_form bf + WHERE bf.id = p_form_id; - v_function_definition := REPLACE( - v_function_definition, - 'ROW_NUMBER() OVER (ORDER BY t.item_id)', - 'ROW_NUMBER() OVER (ORDER BY t.path)' - ); + IF v_form_type IS DISTINCT FROM 'FORM_2' THEN + RETURN; + END IF; - EXECUTE v_function_definition; + + + + + FOR v_rec IN + WITH limit_amounts AS ( + SELECT + ei.id AS expense_item_id, + regexp_replace(lt.name, '^[[:space:]]*-[[:space:]]*', '')::varchar AS line_name, + COALESCE(fl.qty_q1, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q1, + COALESCE(fl.qty_q2, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q2, + COALESCE(fl.qty_q3, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q3, + COALESCE(fl.qty_q4, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q4 + FROM v3.limit_template lt + LEFT JOIN v3.form_limit fl + ON fl.template_id = lt.id + AND fl.budget_form_id = p_form_id + JOIN v3.expense_item ei + ON ei.sheet = 'AHR' + AND ei.direction = 'Support' + AND ei.depth = 3 + AND (ei.item_id = lt.expense_item_code OR ei.num_group_id = lt.expense_item_code) + JOIN v3.expense_item_form_type eft + ON eft.expense_item_id = ei.id + AND eft.form_type_code = 'FORM_2' + WHERE lt.row_type = 'LEAF' + ) + SELECT * FROM limit_amounts + LOOP + PERFORM v3._upsert_form2_ahr_limit_sync_values( + p_form_id, v_rec.expense_item_id, v_rec.line_name, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4 + ); + END LOOP; + + + FOR v_line_id IN + SELECT m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + JOIN v3.budget_line bl ON bl.id = m.target_line_id + WHERE m.source_sheet = 'AHR_RENT' + AND bl.budget_form_id = p_form_id + AND NOT EXISTS (SELECT 1 FROM v3.rent_detail rd WHERE rd.id = m.source_id) + LOOP + DELETE FROM v3.plan WHERE line_id = v_line_id; + DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id; + DELETE FROM v3.budget_line WHERE id = v_line_id; + END LOOP; + + FOR v_rec IN + SELECT + rd.id AS source_id, + rd.line_id AS existing_line_id, + rd.vsp_id, + COALESCE(v.format, 'АХР Аренда #' || rd.id)::varchar AS line_name, + rd.contract_number::varchar AS internal_order, + rd.plan_q1, rd.plan_q2, rd.plan_q3, rd.plan_q4, + rd.actual_jan, rd.actual_feb, rd.actual_mar, + rd.actual_apr, rd.actual_may, rd.actual_jun, + rd.actual_jul, rd.actual_aug, rd.actual_sep, + rd.actual_oct, rd.actual_nov, rd.actual_dec + FROM v3.rent_detail rd + JOIN v3.budget_line bl ON bl.id = rd.line_id + LEFT JOIN v3.vsp v ON v.id = rd.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY rd.id + LOOP + PERFORM v3._upsert_form2_ahr_detail_sync_values( + p_form_id, 314, 'AHR_RENT', v_rec.source_id, + v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4, + v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar, + v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun, + v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep, + v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec + ); + + UPDATE v3.rent_detail rd + SET line_id = m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + WHERE rd.id = v_rec.source_id + AND m.source_sheet = 'AHR_RENT' + AND m.source_id = rd.id; + END LOOP; + + + FOR v_line_id IN + SELECT m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + JOIN v3.budget_line bl ON bl.id = m.target_line_id + WHERE m.source_sheet = 'AHR_UTILITY' + AND bl.budget_form_id = p_form_id + AND NOT EXISTS (SELECT 1 FROM v3.utility_detail ud WHERE ud.id = m.source_id) + LOOP + DELETE FROM v3.plan WHERE line_id = v_line_id; + DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id; + DELETE FROM v3.budget_line WHERE id = v_line_id; + END LOOP; + + FOR v_rec IN + SELECT + ud.id AS source_id, + ud.line_id AS existing_line_id, + ud.vsp_id, + COALESCE(v.format, 'АХР Коммунальные услуги #' || ud.id)::varchar AS line_name, + ud.contract_number::varchar AS internal_order, + ud.plan_q1, ud.plan_q2, ud.plan_q3, ud.plan_q4, + ud.actual_jan, ud.actual_feb, ud.actual_mar, + ud.actual_apr, ud.actual_may, ud.actual_jun, + ud.actual_jul, ud.actual_aug, ud.actual_sep, + ud.actual_oct, ud.actual_nov, ud.actual_dec + FROM v3.utility_detail ud + JOIN v3.budget_line bl ON bl.id = ud.line_id + LEFT JOIN v3.vsp v ON v.id = ud.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY ud.id + LOOP + PERFORM v3._upsert_form2_ahr_detail_sync_values( + p_form_id, 24, 'AHR_UTILITY', v_rec.source_id, + v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4, + v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar, + v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun, + v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep, + v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec + ); + + UPDATE v3.utility_detail ud + SET line_id = m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + WHERE ud.id = v_rec.source_id + AND m.source_sheet = 'AHR_UTILITY' + AND m.source_id = ud.id; + END LOOP; + + + FOR v_line_id IN + SELECT m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + JOIN v3.budget_line bl ON bl.id = m.target_line_id + WHERE m.source_sheet = 'AHR_SECURITY' + AND bl.budget_form_id = p_form_id + AND NOT EXISTS (SELECT 1 FROM v3.security_detail sd WHERE sd.id = m.source_id) + LOOP + DELETE FROM v3.plan WHERE line_id = v_line_id; + DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id; + DELETE FROM v3.budget_line WHERE id = v_line_id; + END LOOP; + + FOR v_rec IN + SELECT + sd.id AS source_id, + sd.line_id AS existing_line_id, + sd.vsp_id, + COALESCE(v.format, 'АХР Безопасность #' || sd.id)::varchar AS line_name, + sd.contract_number::varchar AS internal_order, + sd.plan_q1, sd.plan_q2, sd.plan_q3, sd.plan_q4, + sd.actual_jan, sd.actual_feb, sd.actual_mar, + sd.actual_apr, sd.actual_may, sd.actual_jun, + sd.actual_jul, sd.actual_aug, sd.actual_sep, + sd.actual_oct, sd.actual_nov, sd.actual_dec + FROM v3.security_detail sd + JOIN v3.budget_line bl ON bl.id = sd.line_id + LEFT JOIN v3.vsp v ON v.id = sd.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY sd.id + LOOP + PERFORM v3._upsert_form2_ahr_detail_sync_values( + p_form_id, 779, 'AHR_SECURITY', v_rec.source_id, + v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4, + v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar, + v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun, + v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep, + v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec + ); + + UPDATE v3.security_detail sd + SET line_id = m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + WHERE sd.id = v_rec.source_id + AND m.source_sheet = 'AHR_SECURITY' + AND m.source_id = sd.id; + END LOOP; END; -$block$; +$function$; + +CREATE OR REPLACE FUNCTION v3.sync_form2_ahr_satellite_line(p_form_id integer, p_sheet character varying, p_source_id integer) + RETURNS void + LANGUAGE plpgsql +AS $function$ +DECLARE + v_rec record; +BEGIN + IF p_sheet = 'AHR_LIMIT' THEN + SELECT + ei.id AS expense_item_id, + regexp_replace(lt.name, '^[[:space:]]*-[[:space:]]*', '')::varchar AS line_name, + COALESCE(fl.qty_q1, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q1, + COALESCE(fl.qty_q2, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q2, + COALESCE(fl.qty_q3, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q3, + COALESCE(fl.qty_q4, 0) * COALESCE(lt.limit_with_vat, 0) * + (CASE WHEN lt.unit LIKE '%/год' THEN 0.25 + WHEN lt.unit LIKE '%/месяц' THEN 3 + ELSE 1 END) AS plan_q4 + INTO v_rec + FROM v3.limit_template lt + LEFT JOIN v3.form_limit fl + ON fl.template_id = lt.id + AND fl.budget_form_id = p_form_id + JOIN v3.expense_item ei + ON ei.sheet = 'AHR' + AND ei.direction = 'Support' + AND ei.depth = 3 + AND (ei.item_id = lt.expense_item_code OR ei.num_group_id = lt.expense_item_code) + JOIN v3.expense_item_form_type eft + ON eft.expense_item_id = ei.id + AND eft.form_type_code = 'FORM_2' + WHERE lt.id = p_source_id + AND lt.row_type = 'LEAF'; + + IF v_rec.expense_item_id IS NOT NULL THEN + PERFORM v3._upsert_form2_ahr_limit_sync_values( + p_form_id, v_rec.expense_item_id, v_rec.line_name, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4 + ); + END IF; + RETURN; + END IF; + + IF p_sheet = 'AHR_RENT' THEN + SELECT + rd.id AS source_id, + rd.line_id AS existing_line_id, + rd.vsp_id, + COALESCE(v.format, 'АХР Аренда #' || rd.id)::varchar AS line_name, + rd.contract_number::varchar AS internal_order, + rd.plan_q1, rd.plan_q2, rd.plan_q3, rd.plan_q4, + rd.actual_jan, rd.actual_feb, rd.actual_mar, + rd.actual_apr, rd.actual_may, rd.actual_jun, + rd.actual_jul, rd.actual_aug, rd.actual_sep, + rd.actual_oct, rd.actual_nov, rd.actual_dec + INTO v_rec + FROM v3.rent_detail rd + JOIN v3.budget_line bl ON bl.id = rd.line_id + LEFT JOIN v3.vsp v ON v.id = rd.vsp_id + WHERE rd.id = p_source_id + AND bl.budget_form_id = p_form_id; + + IF v_rec.source_id IS NOT NULL THEN + PERFORM v3._upsert_form2_ahr_detail_sync_values( + p_form_id, 314, 'AHR_RENT', v_rec.source_id, + v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4, + v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar, + v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun, + v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep, + v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec + ); + + UPDATE v3.rent_detail rd + SET line_id = m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + WHERE rd.id = v_rec.source_id + AND m.source_sheet = 'AHR_RENT' + AND m.source_id = rd.id; + END IF; + RETURN; + END IF; + + IF p_sheet = 'AHR_UTILITY' THEN + SELECT + ud.id AS source_id, + ud.line_id AS existing_line_id, + ud.vsp_id, + COALESCE(v.format, 'АХР Коммунальные услуги #' || ud.id)::varchar AS line_name, + ud.contract_number::varchar AS internal_order, + ud.plan_q1, ud.plan_q2, ud.plan_q3, ud.plan_q4, + ud.actual_jan, ud.actual_feb, ud.actual_mar, + ud.actual_apr, ud.actual_may, ud.actual_jun, + ud.actual_jul, ud.actual_aug, ud.actual_sep, + ud.actual_oct, ud.actual_nov, ud.actual_dec + INTO v_rec + FROM v3.utility_detail ud + JOIN v3.budget_line bl ON bl.id = ud.line_id + LEFT JOIN v3.vsp v ON v.id = ud.vsp_id + WHERE ud.id = p_source_id + AND bl.budget_form_id = p_form_id; + + IF v_rec.source_id IS NOT NULL THEN + PERFORM v3._upsert_form2_ahr_detail_sync_values( + p_form_id, 24, 'AHR_UTILITY', v_rec.source_id, + v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4, + v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar, + v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun, + v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep, + v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec + ); + + UPDATE v3.utility_detail ud + SET line_id = m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + WHERE ud.id = v_rec.source_id + AND m.source_sheet = 'AHR_UTILITY' + AND m.source_id = ud.id; + END IF; + RETURN; + END IF; + + IF p_sheet = 'AHR_SECURITY' THEN + SELECT + sd.id AS source_id, + sd.line_id AS existing_line_id, + sd.vsp_id, + COALESCE(v.format, 'АХР Безопасность #' || sd.id)::varchar AS line_name, + sd.contract_number::varchar AS internal_order, + sd.plan_q1, sd.plan_q2, sd.plan_q3, sd.plan_q4, + sd.actual_jan, sd.actual_feb, sd.actual_mar, + sd.actual_apr, sd.actual_may, sd.actual_jun, + sd.actual_jul, sd.actual_aug, sd.actual_sep, + sd.actual_oct, sd.actual_nov, sd.actual_dec + INTO v_rec + FROM v3.security_detail sd + JOIN v3.budget_line bl ON bl.id = sd.line_id + LEFT JOIN v3.vsp v ON v.id = sd.vsp_id + WHERE sd.id = p_source_id + AND bl.budget_form_id = p_form_id; + + IF v_rec.source_id IS NOT NULL THEN + PERFORM v3._upsert_form2_ahr_detail_sync_values( + p_form_id, 779, 'AHR_SECURITY', v_rec.source_id, + v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id, + v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4, + v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar, + v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun, + v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep, + v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec + ); + + UPDATE v3.security_detail sd + SET line_id = m.target_line_id + FROM v3.form2_ahr_satellite_line_source m + WHERE sd.id = v_rec.source_id + AND m.source_sheet = 'AHR_SECURITY' + AND m.source_id = sd.id; + END IF; + END IF; +END; +$function$; + +CREATE OR REPLACE FUNCTION v3.add_budget_line(p_form_id integer, p_expense_item_id integer DEFAULT NULL::integer, p_sheet character varying DEFAULT NULL::character varying, p_item_id character varying DEFAULT NULL::character varying, p_section_code character varying DEFAULT NULL::character varying, p_direction character varying DEFAULT NULL::character varying, p_name character varying DEFAULT NULL::character varying, p_internal_order character varying DEFAULT NULL::character varying, p_vsp_id integer DEFAULT NULL::integer, p_project_id integer DEFAULT NULL::integer, p_justification character varying DEFAULT NULL::character varying, p_contract_number character varying DEFAULT NULL::character varying, p_contract_end_date date DEFAULT NULL::date) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql +AS $function$ +DECLARE + v_form_type VARCHAR; + v_eid INT; + v_sheet VARCHAR; + v_auto_dir VARCHAR; + v_final_dir VARCHAR; + v_new_id INT; + v_cnt INT; + v_sat_table TEXT; + v_sat_eid INT; + v_parent_line INT; + v_decision JSONB; + v_msg TEXT; + v_user_id INT; + v_line_name VARCHAR; +BEGIN + SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id; + IF v_form_type IS NULL THEN + RAISE EXCEPTION 'budget_form id=% не существует', p_form_id; + END IF; + IF v_form_type = 'FORM_3' THEN + RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)'; + END IF; + + v_user_id := v3.current_user_id(); + v_decision := v3.can_edit(p_form_id, p_sheet, p_direction, NULL, v_user_id); + + IF NOT (v_decision->>'ok')::boolean THEN + IF v_decision->>'code' = 'role_not_allowed' THEN + RAISE EXCEPTION 'role_not_allowed: Невозможно вставить строку'; + ELSIF v_decision->>'code' = 'org_not_assigned' THEN + RAISE EXCEPTION 'org_not_assigned: Невозможно вставить строку'; + ELSIF v_decision->>'code' = 'window_closed' THEN + IF v_decision ? 'opens_at' THEN + v_msg := format('window_closed: opens %s', v_decision->>'opens_at'); + ELSIF v_decision->>'closes_at' IS NOT NULL THEN + v_msg := format('window_closed: closed %s', v_decision->>'closes_at'); + ELSE + v_msg := 'window_closed: no phase'; + END IF; + RAISE EXCEPTION '%', v_msg; + ELSE + RAISE EXCEPTION '%', v_decision->>'code'; + END IF; + END IF; + + IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN + v_sat_table := CASE p_sheet + WHEN 'AHR_RENT' THEN 'rent_detail' + WHEN 'AHR_UTILITY' THEN 'utility_detail' + WHEN 'AHR_SECURITY' THEN 'security_detail' + END; + v_sat_eid := CASE p_sheet + WHEN 'AHR_RENT' THEN 314 + WHEN 'AHR_UTILITY' THEN 24 + WHEN 'AHR_SECURITY' THEN 779 + END; + + IF p_vsp_id IS NULL THEN + RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet; + END IF; + IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN + RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id; + END IF; + + SELECT id INTO v_parent_line + FROM v3.budget_line + WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid + LIMIT 1; + IF v_parent_line IS NULL THEN + INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction) + VALUES ( + p_form_id, v_sat_eid, + CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END + ) RETURNING id INTO v_parent_line; + END IF; + + EXECUTE format( + 'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) ' + || 'VALUES ($1, $2, $3, $4) RETURNING id', + v_sat_table + ) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date + INTO v_new_id; + + PERFORM v3.sync_form2_ahr_satellite_line(p_form_id, p_sheet, v_new_id); + + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data + FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v + WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id; + RETURN; + END IF; + + IF p_expense_item_id IS NOT NULL THEN + SELECT ei.id, ei.sheet INTO v_eid, v_sheet + FROM v3.expense_item ei WHERE ei.id = p_expense_item_id; + IF v_eid IS NULL THEN + RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id; + END IF; + IF NOT EXISTS ( + SELECT 1 FROM v3.expense_item_form_type + WHERE expense_item_id = v_eid AND form_type_code = v_form_type + ) THEN + RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type; + END IF; + ELSE + IF p_sheet IS NULL OR p_item_id IS NULL THEN + RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)'; + END IF; + + SELECT count(*) INTO v_cnt + FROM v3.expense_item ei + JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id + WHERE ei.item_id = p_item_id + AND ei.sheet = p_sheet + AND eift.form_type_code = v_form_type + AND (p_section_code IS NULL OR ei.section_code = p_section_code) + AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction); + + IF v_cnt = 0 THEN + RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%', + p_sheet, p_item_id, p_section_code, p_direction, v_form_type; + END IF; + IF v_cnt > 1 THEN + RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую', + v_cnt, p_sheet, p_item_id, p_section_code, p_direction; + END IF; + + SELECT ei.id, ei.sheet INTO v_eid, v_sheet + FROM v3.expense_item ei + JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id + WHERE ei.item_id = p_item_id + AND ei.sheet = p_sheet + AND eift.form_type_code = v_form_type + AND (p_section_code IS NULL OR ei.section_code = p_section_code) + AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction); + END IF; + + v_auto_dir := CASE + WHEN v_form_type = 'FORM_2' THEN 'Support' + WHEN v_form_type = 'FORM_4' THEN 'Development' + WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL + ELSE NULL + END; + + IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN + RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet; + END IF; + + IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN + RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)', + p_direction, v_form_type, v_auto_dir; + END IF; + + IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN + RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction; + END IF; + + v_final_dir := COALESCE(p_direction, v_auto_dir); + + IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN + RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id; + END IF; + IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.form4_project WHERE id = p_project_id) THEN + RAISE EXCEPTION 'form4_project id=% не существует', p_project_id; + END IF; + + v_line_name := CASE WHEN v_form_type = 'FORM_4' THEN NULL ELSE '' END; + + INSERT INTO v3.budget_line ( + budget_form_id, expense_item_id, name, internal_order, + vsp_id, project_id, direction, justification + ) VALUES ( + p_form_id, v_eid, v_line_name, p_internal_order, + p_vsp_id, p_project_id, v_final_dir, p_justification + ) RETURNING id INTO v_new_id; + + PERFORM v3.log_event( + 'ROW_CREATE', 'ROW', + jsonb_build_object( + 'sheet', v_sheet, + 'line_id', v_new_id, + 'expense_item_id', v_eid, + 'direction', v_final_dir, + 'name', v_line_name, + 'vsp_id', p_vsp_id, + 'project_id', p_project_id + ), + p_form_id + ); + + DECLARE v_ei_path INT[]; + BEGIN + v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]); + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data + FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v + WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM') + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) + OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id) + ORDER BY v.sort_order; + END; +END; +$function$; + +CREATE OR REPLACE FUNCTION v3.v_summary_view(p_year integer, p_sheet character varying, p_org_unit_ids integer[] DEFAULT NULL::integer[]) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql + STABLE + SET search_path TO 'v3', 'pg_catalog' +AS $function$ +BEGIN + + IF p_sheet = 'FORM_1' THEN + RETURN QUERY + SELECT + (CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' ELSE 'ITEM' END)::character varying, + t.depth, + ROW_NUMBER() OVER (ORDER BY t.path), + jsonb_build_object( + 'section_code', s.section_code, + 'name', s.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', s.plan_support_q1, 'q2', s.plan_support_q2, 'q3', s.plan_support_q3, 'q4', s.plan_support_q4, 'year', s.plan_support_year), + 'development', jsonb_build_object('q1', s.plan_development_q1, 'q2', s.plan_development_q2, 'q3', s.plan_development_q3, 'q4', s.plan_development_q4, 'year', s.plan_development_year), + 'total_year', s.plan_total_year + ), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', s.approved_support_q1, 'q2', s.approved_support_q2, 'q3', s.approved_support_q3, 'q4', s.approved_support_q4, 'year', s.approved_support_year), + 'development', jsonb_build_object('q1', s.approved_development_q1, 'q2', s.approved_development_q2, 'q3', s.approved_development_q3, 'q4', s.approved_development_q4, 'year', s.approved_development_year), + 'total_year', s.approved_total_year + ), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', s.fact_support_q1, 'q2', s.fact_support_q2, 'q3', s.fact_support_q3, 'q4', s.fact_support_q4, 'year', s.fact_support_year), + 'development', jsonb_build_object('q1', s.fact_development_q1, 'q2', s.fact_development_q2, 'q3', s.fact_development_q3, 'q4', s.fact_development_q4, 'year', s.fact_development_year), + 'total_year', s.fact_total_year + ), + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', s.corrected_support_q2, 'q3', s.corrected_support_q3, 'q4', s.corrected_support_q4), + 'development', jsonb_build_object('q2', s.corrected_development_q2, 'q3', s.corrected_development_q3, 'q4', s.corrected_development_q4) + ) + ) + FROM v3.v_1form_ssp(p_year, p_org_unit_ids) s + JOIN v3.mv_expense_item_tree t ON t.item_id = s.section_code AND t.depth < 3; + RETURN; + END IF; + + IF p_sheet = 'FORM_2' THEN + RETURN QUERY + SELECT + (CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' ELSE 'ITEM' END)::character varying, + t.depth, + ROW_NUMBER() OVER (ORDER BY t.path), + jsonb_build_object( + 'section_code', s.section_code, + 'name', s.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', s.plan_support_q1, 'q2', s.plan_support_q2, 'q3', s.plan_support_q3, 'q4', s.plan_support_q4, 'year', s.plan_support_year), + 'development', NULL + ), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', s.approved_support_q1, 'q2', s.approved_support_q2, 'q3', s.approved_support_q3, 'q4', s.approved_support_q4, 'year', s.approved_support_year), + 'development', NULL + ), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', s.fact_support_q1, 'q2', s.fact_support_q2, 'q3', s.fact_support_q3, 'q4', s.fact_support_q4, 'year', s.fact_support_year), + 'development', NULL + ), + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', s.corrected_support_q2, 'q3', s.corrected_support_q3, 'q4', s.corrected_support_q4), + 'development', NULL + ) + ) + FROM v3.v_2form_rf_p(p_year, p_org_unit_ids) s + JOIN v3.mv_expense_item_tree t ON t.item_id = s.section_code AND t.depth < 3; + RETURN; + END IF; + + IF p_sheet = 'FORM_3' THEN + RETURN QUERY + SELECT + (CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' ELSE 'ITEM' END)::character varying, + t.depth, + ROW_NUMBER() OVER (ORDER BY t.path), + jsonb_build_object( + 'section_code', s.section_code, + 'name', s.name, + 'plan', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q1', s.plan_development_q1, 'q2', s.plan_development_q2, 'q3', s.plan_development_q3, 'q4', s.plan_development_q4, 'year', s.plan_development_year) + ), + 'approved', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q1', s.approved_development_q1, 'q2', s.approved_development_q2, 'q3', s.approved_development_q3, 'q4', s.approved_development_q4, 'year', s.approved_development_year) + ), + 'fact', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q1', s.fact_development_q1, 'q2', s.fact_development_q2, 'q3', s.fact_development_q3, 'q4', s.fact_development_q4, 'year', s.fact_development_year) + ), + 'corrected', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q2', s.corrected_development_q2, 'q3', s.corrected_development_q3, 'q4', s.corrected_development_q4) + ) + ) + FROM v3.v_3form_rf_r(p_year, p_org_unit_ids) s + JOIN v3.mv_expense_item_tree t ON t.item_id = s.section_code AND t.depth < 3; + RETURN; + END IF; + + IF p_sheet = 'FORM_4' THEN + RETURN QUERY + SELECT + (CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' ELSE 'ITEM' END)::character varying, + t.depth, + ROW_NUMBER() OVER (ORDER BY t.path), + jsonb_build_object( + 'section_code', s.section_code, + 'name', s.name, + 'plan', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q1', s.plan_development_q1, 'q2', s.plan_development_q2, 'q3', s.plan_development_q3, 'q4', s.plan_development_q4, 'year', s.plan_development_year) + ), + 'approved', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q1', s.approved_development_q1, 'q2', s.approved_development_q2, 'q3', s.approved_development_q3, 'q4', s.approved_development_q4, 'year', s.approved_development_year) + ), + 'fact', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q1', s.fact_development_q1, 'q2', s.fact_development_q2, 'q3', s.fact_development_q3, 'q4', s.fact_development_q4, 'year', s.fact_development_year) + ), + 'corrected', jsonb_build_object( + 'support', NULL, + 'development', jsonb_build_object('q1', s.corrected_development_q1, 'q2', s.corrected_development_q2, 'q3', s.corrected_development_q3, 'q4', s.corrected_development_q4, 'year', s.corrected_development_year) + ) + ) + FROM v3.v_4form_project(p_year, p_org_unit_ids) s + JOIN v3.mv_expense_item_tree t ON t.item_id = s.section_code AND t.depth < 3; + RETURN; + END IF; + + IF p_sheet = 'MAIN' THEN + RETURN QUERY + SELECT + (CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' ELSE 'ITEM' END)::character varying, + t.depth, + ROW_NUMBER() OVER (ORDER BY t.path), + jsonb_build_object( + 'section_code', s.section_code, + 'name', s.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', s.plan_support_q1, 'q2', s.plan_support_q2, 'q3', s.plan_support_q3, 'q4', s.plan_support_q4, 'year', s.plan_support_year), + 'development', jsonb_build_object('q1', s.plan_development_q1, 'q2', s.plan_development_q2, 'q3', s.plan_development_q3, 'q4', s.plan_development_q4, 'year', s.plan_development_year), + 'total_year', s.plan_total_year + ), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', s.approved_support_q1, 'q2', s.approved_support_q2, 'q3', s.approved_support_q3, 'q4', s.approved_support_q4, 'year', s.approved_support_year), + 'development', jsonb_build_object('q1', s.approved_development_q1, 'q2', s.approved_development_q2, 'q3', s.approved_development_q3, 'q4', s.approved_development_q4, 'year', s.approved_development_year), + 'total_year', s.approved_total_year + ), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', s.fact_support_q1, 'q2', s.fact_support_q2, 'q3', s.fact_support_q3, 'q4', s.fact_support_q4, 'year', s.fact_support_year), + 'development', jsonb_build_object('q1', s.fact_development_q1, 'q2', s.fact_development_q2, 'q3', s.fact_development_q3, 'q4', s.fact_development_q4, 'year', s.fact_development_year), + 'total_year', s.fact_total_year + ), + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', s.corrected_support_q2, 'q3', s.corrected_support_q3, 'q4', s.corrected_support_q4), + 'development', jsonb_build_object('q2', s.corrected_development_q2, 'q3', s.corrected_development_q3, 'q4', s.corrected_development_q4) + ) + ) + FROM v3.v_main_format(p_year, p_org_unit_ids) s + JOIN v3.mv_expense_item_tree t ON t.item_id = s.section_code AND t.depth < 3; + RETURN; + END IF; + + RAISE EXCEPTION 'Unknown summary sheet: %', p_sheet + USING HINT = 'Use FORM_1 / FORM_2 / FORM_3 / FORM_4 / MAIN'; + +END; +$function$; ALTER TABLE v3.expense_item ADD CONSTRAINT expense_item_expense_item_fk -- 2.47.2