-- ════════════════════════════════════════════════════════════════════════════ -- МИГРАЦИЯ: Автогенерация technical_number + запрет ручного редактирования -- Формат: YYMM_<код РФ/ВСП 4 цифры><порядковый счётчик 2 цифры> -- Пример: 2606_180801 (26 — год, 06 — месяц, 1808 — org_unit id, 01 — счётчик) -- ════════════════════════════════════════════════════════════════════════════ -- Уникальность технического номера проекта CREATE UNIQUE INDEX IF NOT EXISTS uq_v3_project_technical_number ON v3.project (technical_number); -- add_project: генерируем technical_number автоматически при создании проекта CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying) RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer) LANGUAGE plpgsql AS $function$ DECLARE v_pid INT; v_lim INT; v_cur INT; v_seq INT; v_technical_number VARCHAR; BEGIN IF p_name IS NULL OR length(trim(p_name)) = 0 THEN RAISE EXCEPTION 'p_name обязателен'; END IF; IF p_year IS NULL THEN RAISE EXCEPTION 'p_year обязателен'; END IF; IF p_org_unit_id IS NULL THEN RAISE EXCEPTION 'p_org_unit_id обязателен'; END IF; IF NOT EXISTS (SELECT 1 FROM v3.org_unit ou WHERE id = p_org_unit_id AND ou.is_active) THEN RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id; END IF; IF p_parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id; END IF; -- Порядковый счётчик проекта для данного РФ/ВСП (org_unit) SELECT COALESCE(MAX(right(technical_number, 2)::int), 0) + 1 INTO v_seq FROM v3.project WHERE org_unit_id = p_org_unit_id AND technical_number IS NOT NULL; v_technical_number := to_char(now(), 'YYMM') || '_' || lpad(p_org_unit_id::text, 4, '0') || lpad(v_seq::text, 2, '0'); INSERT INTO v3.project ( name, level, parent_id, org_unit_id, project_type, vsp_format, placement_type, object_address, staff_count, total_area, system_code, technical_number ) VALUES ( p_name, p_level, p_parent_id, p_org_unit_id, p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area, p_system_code, v_technical_number ) RETURNING id INTO v_pid; -- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей. SELECT y.limit_report_id, y.current_expenses_report_id INTO v_lim, v_cur FROM v3.add_project_year(v_pid, p_year, false) y; -- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов PERFORM v3.log_event( 'PROJECT_CREATE', 'PROJECT', jsonb_strip_nulls(jsonb_build_object( 'project_id', v_pid, 'name', p_name, 'year', p_year, 'level', p_level, 'parent_id', p_parent_id, 'org_unit_id', p_org_unit_id, 'project_type', p_project_type, 'vsp_format', p_vsp_format, 'placement_type', p_placement_type, 'object_address', p_object_address, 'system_code', p_system_code, 'technical_number', v_technical_number, 'staff_count', p_staff_count, 'total_area', p_total_area, 'limit_report_id', v_lim, 'current_expenses_report_id', v_cur )) ); RETURN QUERY SELECT v_pid, v_lim, v_cur; END; $function$ ; -- upd_project: убираем параметр technical_number (редактировать больше нельзя) DROP FUNCTION IF EXISTS v3.upd_project(integer, varchar, varchar, varchar, varchar, varchar, varchar, varchar, integer, numeric, integer, date, date, date, date, varchar); CREATE OR REPLACE FUNCTION v3.upd_project( p_project_id integer, p_name character varying DEFAULT NULL, p_status character varying DEFAULT NULL, p_project_type character varying DEFAULT NULL, p_vsp_format character varying DEFAULT NULL, p_placement_type character varying DEFAULT NULL, p_object_address character varying DEFAULT NULL, p_staff_count integer DEFAULT NULL, p_total_area numeric DEFAULT NULL, p_org_unit_id integer DEFAULT NULL, p_krf_decision_date date DEFAULT NULL, p_fk_decision_date date DEFAULT NULL, p_board_decision_date date DEFAULT NULL, p_open_relocate_close_date date DEFAULT NULL, p_funding_by_ko_decision character varying DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE v_old v3.project%ROWTYPE; v_changes JSONB := '{}'::jsonb; BEGIN SELECT * INTO v_old FROM v3.project WHERE id = p_project_id; IF NOT FOUND THEN RAISE EXCEPTION 'project id=% не существует', p_project_id; END IF; IF p_name IS NOT NULL AND p_name IS DISTINCT FROM v_old.name THEN UPDATE v3.project SET name = p_name WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('name', jsonb_build_object('before', v3._diff_val(v_old.name), 'after', v3._diff_val(p_name))); END IF; IF p_status IS NOT NULL AND p_status IS DISTINCT FROM v_old.status THEN UPDATE v3.project SET status = p_status WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('before', v3._diff_val(v_old.status), 'after', v3._diff_val(p_status))); END IF; IF p_project_type IS NOT NULL AND p_project_type IS DISTINCT FROM v_old.project_type THEN UPDATE v3.project SET project_type = p_project_type WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('project_type', jsonb_build_object('before', v3._diff_val(v_old.project_type), 'after', v3._diff_val(p_project_type))); END IF; IF p_vsp_format IS NOT NULL AND p_vsp_format IS DISTINCT FROM v_old.vsp_format THEN UPDATE v3.project SET vsp_format = p_vsp_format WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('vsp_format', jsonb_build_object('before', v3._diff_val(v_old.vsp_format), 'after', v3._diff_val(p_vsp_format))); END IF; IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN UPDATE v3.project SET placement_type = p_placement_type WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('placement_type', jsonb_build_object('before', v3._diff_val(v_old.placement_type), 'after', v3._diff_val(p_placement_type))); END IF; IF p_object_address IS NOT NULL AND p_object_address IS DISTINCT FROM v_old.object_address THEN UPDATE v3.project SET object_address = p_object_address WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('object_address', jsonb_build_object('before', v3._diff_val(v_old.object_address), 'after', v3._diff_val(p_object_address))); END IF; IF p_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN UPDATE v3.project SET staff_count = p_staff_count WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('staff_count', jsonb_build_object('before', v3._diff_val(v_old.staff_count), 'after', v3._diff_val(p_staff_count))); END IF; IF p_total_area IS NOT NULL AND p_total_area IS DISTINCT FROM v_old.total_area THEN UPDATE v3.project SET total_area = p_total_area WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('total_area', jsonb_build_object('before', v3._diff_val(v_old.total_area), 'after', v3._diff_val(p_total_area))); END IF; IF p_org_unit_id IS NOT NULL AND p_org_unit_id IS DISTINCT FROM v_old.org_unit_id THEN IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id; END IF; UPDATE v3.project SET org_unit_id = p_org_unit_id WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('org_unit_id', jsonb_build_object('before', v3._diff_val(v_old.org_unit_id), 'after', v3._diff_val(p_org_unit_id))); END IF; IF p_krf_decision_date IS NOT NULL AND p_krf_decision_date IS DISTINCT FROM v_old.krf_decision_date THEN UPDATE v3.project SET krf_decision_date = p_krf_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('krf_decision_date', jsonb_build_object('before', v3._diff_val(v_old.krf_decision_date), 'after', v3._diff_val(p_krf_decision_date))); END IF; IF p_fk_decision_date IS NOT NULL AND p_fk_decision_date IS DISTINCT FROM v_old.fk_decision_date THEN UPDATE v3.project SET fk_decision_date = p_fk_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('fk_decision_date', jsonb_build_object('before', v3._diff_val(v_old.fk_decision_date), 'after', v3._diff_val(p_fk_decision_date))); END IF; IF p_board_decision_date IS NOT NULL AND p_board_decision_date IS DISTINCT FROM v_old.board_decision_date THEN UPDATE v3.project SET board_decision_date = p_board_decision_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('board_decision_date', jsonb_build_object('before', v3._diff_val(v_old.board_decision_date), 'after', v3._diff_val(p_board_decision_date))); END IF; IF p_open_relocate_close_date IS NOT NULL AND p_open_relocate_close_date IS DISTINCT FROM v_old.open_relocate_close_date THEN UPDATE v3.project SET open_relocate_close_date = p_open_relocate_close_date WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('open_relocate_close_date', jsonb_build_object('before', v3._diff_val(v_old.open_relocate_close_date), 'after', v3._diff_val(p_open_relocate_close_date))); END IF; IF p_funding_by_ko_decision IS NOT NULL AND p_funding_by_ko_decision IS DISTINCT FROM v_old.funding_by_ko_decision THEN UPDATE v3.project SET funding_by_ko_decision = p_funding_by_ko_decision WHERE id = p_project_id; v_changes := v_changes || jsonb_build_object('funding_by_ko_decision', jsonb_build_object('before', v3._diff_val(v_old.funding_by_ko_decision), 'after', v3._diff_val(p_funding_by_ko_decision))); END IF; IF v_changes <> '{}'::jsonb THEN PERFORM v3.log_event( 'PROJECT_UPDATE', 'PROJECT', jsonb_build_object( 'project_id', p_project_id, 'entity_id', p_project_id, 'changes', v_changes ), null, null, COALESCE(p_org_unit_id, v_old.org_unit_id) ); END IF; END; $function$ ; UPDATE v3.phase_template SET column_keys = '{plan.q1,plan.q2,plan.q3,plan.q4,plan.year,plan.comment,contract_summary.total,contract_summary.reference,contract_summary.counterparty,contract_summary.deadline,contract_summary.comment,contract_summary.future_y1,contract_summary.future_y2,contract_summary.other_ssp,contract_summary.centralized_flag,allocation.contract_ref,allocation.allocation_purpose,seq_dfip.q1,seq_dfip.q2,seq_dfip.q3,seq_dfip.q4,seq_dfip.year,seq_dfip.justification,reserve.q1,reserve.q2,reserve.q3,reserve.q4,reserve.year,reserve.justification,approved.q1,approved.q2,approved.q3,approved.q4,approved.year,collegial.approved,collegial.protocol,collegial.note,ckk.ceiling,ckk.q1,ckk.q2,ckk.q3,ckk.q4,ckk.rf_schedule,ckk.deadline,ckk.proc_plan,ckk.proc_method,ckk.comment,contract.counterparty,contract.reference,contract.addenda,contract.subject,contract.currency,contract.ceiling,contract.q1,contract.q2,contract.q3,contract.q4,contract.rf_schedule,contract.vat_rate,contract.exchange_rate,contract.amount_foreign,contract.deadline,contract.scheme,contract.act,contract.comment,q1.adj_current,q1.adj_ssp,q1.adj_rf,q1.adj_reserve,q1.adj_comment,q1.corrected_plan,q1.pay_date,q1.pay_amount,q1.pay_ho,q1.pay_rf,q1.pay_comment,q1.pay_act,q1.booking,q1.actual_m1,q1.actual_m2,q1.actual_m3,q1.actual_quarter,q1.residual_after_booking,q1.residual_after_actual,q1.transfer_q2,q1.transfer_q3,q1.transfer_q4,q1.transfer_econ,q1.total,q2.target_change,q2.base_correction,q2.revision_inc,q2.revision_seq,q2.revision_comment,q2.new_plan,q2.adj_current,q2.adj_ssp,q2.adj_rf,q2.adj_reserve,q2.corrected_plan,q2.pay_date,q2.pay_amount,q2.pay_ho,q2.pay_rf,q2.pay_comment,q2.pay_act,q2.booking,q2.actual_m1,q2.actual_m2,q2.actual_m3,q2.actual_quarter,q2.residual_after_booking,q2.residual_after_actual,q2.transfer_q3,q2.transfer_q4,q2.transfer_econ,q2.total,q3.target_change,q3.base_correction,q3.revision_inc,q3.revision_seq,q3.revision_comment,q3.new_plan,q3.adj_current,q3.adj_ssp,q3.adj_rf,q3.adj_reserve,q3.corrected_plan,q3.pay_date,q3.pay_amount,q3.pay_ho,q3.pay_rf,q3.pay_comment,q3.pay_act,q3.booking,q3.actual_m1,q3.actual_m2,q3.actual_m3,q3.actual_quarter,q3.residual_after_booking,q3.residual_after_actual,q3.transfer_q4,q3.transfer_econ,q3.total,q4.target_change,q4.base_correction,q4.revision_inc,q4.revision_seq,q4.revision_comment,q4.new_plan,q4.adj_current,q4.adj_ssp,q4.adj_rf,q4.adj_reserve,q4.corrected_plan,q4.pay_date,q4.pay_amount,q4.pay_ho,q4.pay_rf,q4.pay_comment,q4.pay_act,q4.booking,q4.actual_m1,q4.actual_m2,q4.actual_m3,q4.actual_quarter,q4.actual_spod,q4.residual_after_booking,q4.residual_after_actual,q4.transfer_econ,q4.total,totals.fact_year}' WHERE form_type = 'FORM_4'; ALTER TABLE v3.form_phase DROP CONSTRAINT chk_v4_form_phase_no_admin;