-- Eine Planstelle klonen. -- -- Anforderung 11 aus dem Workshop, beide Hälften: -- -- Planstellen sollen geklont werden können -- Leitende Planstellen soll man nicht klonen können -- -- ═══ Wofür das gut ist ═══ -- -- In der Fertigung sind Planstellen reihenweise gleich: zwölf -- „Maschinenbediener:in" in derselben Abteilung, auf derselben Kostenstelle. -- Über `create_position` ist jede davon dieselbe Eingabe von Hand, und bei -- zwölf Wiederholungen entstehen „Maschinenbediener:in" und -- „Maschinenbediener in" nebeneinander. Die Tätigkeit landet dann als zwei -- Einträge im Katalog, und jede Auswertung nach Tätigkeit ist ab da falsch. -- -- Der Klon nimmt Einheit, Tätigkeit und Kontierung der Vorlage und vergibt -- nur eine neue Nummer. Getippt wird nichts. -- -- ═══ Warum eine Leitung sich nicht klonen lässt ═══ -- -- Es gibt je Einheit genau eine, und ein Unique-Index sichert das ab -- (`is_chief and valid_to is null`). Ein Klon einer Leitungsplanstelle wäre -- also entweder ein Fehler aus dem Maschinenraum — „duplicate key value -- violates unique constraint" — oder, wenn man `is_chief` beim Klonen -- stillschweigend fallen liesse, eine Planstelle, die anders ist als die -- Vorlage, ohne dass es jemand angefordert hat. -- -- Deshalb die ausdrückliche Prüfung mit einer Meldung, die den Grund nennt. -- Sie steht *vor* allem anderen, damit der Fall nicht erst an einem Index -- scheitert. -- -- ═══ Was mitkommt und was nicht ═══ -- -- Mit: Organisationseinheit, Tätigkeit (derselbe Katalogeintrag, nicht eine -- neue Zeile mit demselben Text) und die zum Stichtag geltende Kontierung. -- Ohne Kontierung wäre der Klon eine Planstelle, die niemand bezahlt, und -- genau das Feld müsste man danach doch wieder von Hand nachtragen. -- -- Nicht mit: die Besetzung. Eine Planstelle ist ein Platz, keine Person — -- der Klon ist per Definition frei. create or replace function clone_position(payload jsonb) returns uuid language plpgsql set search_path = public, pg_temp as $$ declare v_vorlage_id uuid := (payload->>'position_id')::uuid; v_valid_from date := coalesce(nullif(payload->>'valid_from','')::date, current_date); v_vorlage om_positions%rowtype; v_job_title text; v_unit_name text; v_neu_id uuid; v_kontierung integer := 0; begin perform require_hr_admin(); select * into v_vorlage from om_positions where id = v_vorlage_id; if v_vorlage.id is null then raise exception 'Die Planstelle existiert nicht.'; end if; if v_vorlage.is_chief then raise exception 'Leitungsplanstellen lassen sich nicht klonen. Je Organisationseinheit gibt es genau eine.'; end if; select j.title, u.name into v_job_title, v_unit_name from jobs j, org_units u where j.id = v_vorlage.job_id and u.id = v_vorlage.org_unit_id; -- Eine Planstelle kann nicht vor ihrer Vorlage beginnen: sie ist deren -- Abbild, und ein Abbild, das früher gilt als das Original, ist keine -- Kopie mehr, sondern eine eigene Geschichte. if v_valid_from < v_vorlage.valid_from then raise exception 'Die Vorlage gilt erst ab %. Der Klon kann nicht früher beginnen.', v_vorlage.valid_from; end if; -- Und nicht nach ihrem Ende: sonst entstünde eine Planstelle, die es zu -- keinem Zeitpunkt gemeinsam mit ihrer Vorlage gab. if v_vorlage.valid_to is not null and v_valid_from >= v_vorlage.valid_to then raise exception 'Die Vorlage gilt nur bis %. Ein Klon ab % wäre nie gleichzeitig gültig.', v_vorlage.valid_to, v_valid_from; end if; insert into om_positions (position_number, org_unit_id, job_id, is_chief, valid_from, valid_to) values (next_position_number(), v_vorlage.org_unit_id, v_vorlage.job_id, false, v_valid_from, v_vorlage.valid_to) returning id into v_neu_id; -- Die zum Stichtag geltende Kontierung mitnehmen. Halboffenes Intervall -- wie überall: valid_from einschliessend, valid_to ausschliessend. insert into position_cost_centers (position_id, cost_center_id, valid_from, valid_to) select v_neu_id, k.cost_center_id, v_valid_from, k.valid_to from position_cost_centers k where k.position_id = v_vorlage_id and k.valid_from <= v_valid_from and (k.valid_to is null or k.valid_to > v_valid_from); get diagnostics v_kontierung = row_count; insert into audit_log (actor_user_id, actor_name, action, target_label, details) values (app_current_user_id(), current_actor_name(), 'Planstelle geklont', v_job_title || ' (' || v_unit_name || ')', 'Kopie von ' || v_vorlage.position_number || ', gültig ab ' || v_valid_from || case when v_kontierung > 0 then ', Kontierung übernommen' else ', ohne Kontierung' end); return v_neu_id; end; $$; comment on function clone_position(jsonb) is 'Legt eine Planstelle als Kopie einer bestehenden an — gleiche Einheit, gleiche Tätigkeit, gleiche Kontierung, neue Nummer, unbesetzt. Leitungsplanstellen sind ausgenommen: je Einheit gibt es genau eine.'; grant execute on function clone_position(jsonb) to alpenwerk_app; -- Selbstprüfung. do $$ declare v_def text; begin if not exists (select 1 from pg_proc where proname = 'clone_position') then raise exception 'clone_position fehlt'; end if; v_def := pg_get_functiondef('public.clone_position(jsonb)'::regprocedure); if v_def not like '%require_hr_admin%' then raise exception 'clone_position prüft die Rechte nicht'; end if; if v_def not like '%is_chief%' then raise exception 'clone_position lässt Leitungsplanstellen durch'; end if; if v_def not like '%position_cost_centers%' then raise exception 'clone_position nimmt die Kontierung nicht mit'; end if; if v_def not like '%audit_log%' then raise exception 'clone_position schreibt nichts ins Protokoll'; end if; -- Ohne search_path liesse sich der Funktion über einen eigenen Schemapfad -- eine andere Tabelle unterschieben. Hauskonvention für jede Funktion. if v_def not like '%search_path%' then raise exception 'clone_position hat keinen festen search_path'; end if; end $$;