-- Eine Planstelle, auf die jemand vorgemerkt ist, ist nicht frei -- -- Gemeldet im Test vom 29.09. (F.24): eine Person wurde auf eine Planstelle -- eingestellt, auf die bereits eine Versetzung zum 20.10. vorgemerkt war. Die -- Planstellenliste führte die Stelle als unbesetzt, der Assistent bot sie an, -- und das Organigramm zum 20.10. zeigte sie zweifach besetzt. -- -- ═══ Warum das schlimmer ist, als es aussieht ═════════════════════ -- -- Eine vorgemerkte Versetzung steht in pending_org_changes und noch nicht in -- position_assignments. „Besetzt?" wird aber allein an position_assignments -- gefragt — die Stelle ist zum Zeitpunkt der Buchung tatsächlich frei. -- -- Auffallen würde es erst in der Nacht zum 20.10.: dann legt der Nachtlauf die -- Besetzung der vorgemerkten Person an und läuft in -- `position_assignments_one_holder`, den Teilindex, der genau eine laufende -- Besetzung je Planstelle zulässt. Der Nachtlauf arbeitet in **einer** -- Transaktion über alle fälligen Vorgänge; eine einzige solche Buchung bringt -- ihn also vollständig zum Stehen — auch für alle anderen. Der Fehler entsteht -- heute und schlägt Wochen später bei jemand anderem zu. -- -- ═══ Was hier steht ═══════════════════════════════════════════════ -- -- Eine Auskunft, die pending_org_changes mitliest, und ihr Aufruf in den vier -- Funktionen, die jemanden auf eine Planstelle setzen: hire_employee, -- rehire_employee, transfer_employee, promote_employee. -- -- Abgewiesen wird mit Datum — „ab dem 20.10.2026 vorgemerkt" sagt der Person -- vor dem Bildschirm, was zu tun ist. Die eigene Vormerkung zählt nicht als -- Hindernis: wer dieselbe Versetzung noch einmal erfasst, korrigiert sie. create or replace function planstelle_vorgemerkt(p_position_id uuid, p_ab date, p_ausser uuid default null) returns date language sql stable set search_path = public, pg_temp as $$ select min(o.effective_date) from pending_org_changes o where o.status = 'pending' -- Nur die beiden Vorgänge, die eine Planstelle belegen. Eine -- Vertragsänderung oder eine Karenz trägt keine Zielplanstelle. and o.change_type in ('transfer', 'promotion') and o.effective_date >= p_ab and (p_ausser is null or o.employee_id <> p_ausser) and nullif(o.payload->>'target_position_id', '')::uuid = p_position_id; $$; comment on function planstelle_vorgemerkt(uuid, date, uuid) is 'Ab wann eine Planstelle durch eine vorgemerkte Versetzung oder Beförderung belegt ist, oder null. Die Vormerkung der übergebenen Person zählt nicht mit.'; grant execute on function planstelle_vorgemerkt(uuid, date, uuid) to alpenwerk_app; -- ═══ hire_employee ════════════════════════════════════════════════ do $migration$ declare v_alt constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_entry);$anker$; v_neu constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_entry); declare v_vorgemerkt date := planstelle_vorgemerkt(v_position_id, v_entry, null); begin if v_vorgemerkt is not null then raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.', to_char(v_vorgemerkt, 'DD.MM.YYYY'); end if; end; $anker$; v_def text; v_anzahl int; begin select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'hire_employee' and p.prokind = 'f'; if v_def is null then raise exception 'hire_employee ist nicht vorhanden.'; end if; v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt); if v_anzahl <> 1 then raise exception 'Der Anker in hire_employee kommt % mal vor, erwartet einmal.', v_anzahl; end if; execute replace(v_def, v_alt, v_neu); end $migration$; -- ═══ rehire_employee ══════════════════════════════════════════════ do $migration$ declare v_alt constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_date);$anker$; v_neu constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_date); declare v_vorgemerkt date := planstelle_vorgemerkt(v_position_id, v_date, v_employee_id); begin if v_vorgemerkt is not null then raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.', to_char(v_vorgemerkt, 'DD.MM.YYYY'); end if; end; $anker$; v_def text; v_anzahl int; begin select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'rehire_employee' and p.prokind = 'f'; if v_def is null then raise exception 'rehire_employee ist nicht vorhanden.'; end if; v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt); if v_anzahl <> 1 then raise exception 'Der Anker in rehire_employee kommt % mal vor, erwartet einmal.', v_anzahl; end if; execute replace(v_def, v_alt, v_neu); end $migration$; -- ═══ transfer_employee ════════════════════════════════════════════ do $migration$ declare v_alt constant text := $anker$where pa.position_id = v_target_position and pa.valid_to is null;$anker$; v_neu constant text := $anker$where pa.position_id = v_target_position and pa.valid_to is null; declare v_vorgemerkt date := planstelle_vorgemerkt(v_target_position, v_effective, v_employee_id); begin if v_vorgemerkt is not null then raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.', to_char(v_vorgemerkt, 'DD.MM.YYYY'); end if; end; $anker$; v_def text; v_anzahl int; begin select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f'; if v_def is null then raise exception 'transfer_employee ist nicht vorhanden.'; end if; v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt); if v_anzahl <> 1 then raise exception 'Der Anker in transfer_employee kommt % mal vor, erwartet einmal.', v_anzahl; end if; execute replace(v_def, v_alt, v_neu); end $migration$; -- ═══ promote_employee ═════════════════════════════════════════════ -- -- Ohne Zielplanstelle ist v_position null; die Auskunft liefert dann nichts, -- und der Zweig läuft durch. Eine Beförderung auf derselben Stelle braucht -- deshalb keine Sonderbehandlung. do $migration$ declare v_alt constant text := $anker$where pa.position_id = v_position and pa.valid_to is null;$anker$; v_neu constant text := $anker$where pa.position_id = v_position and pa.valid_to is null; declare v_vorgemerkt date := planstelle_vorgemerkt(v_position, v_effective_date, v_employee_id); begin if v_vorgemerkt is not null then raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.', to_char(v_vorgemerkt, 'DD.MM.YYYY'); end if; end; $anker$; v_def text; v_anzahl int; begin select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f'; if v_def is null then raise exception 'promote_employee ist nicht vorhanden.'; end if; v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt); if v_anzahl <> 1 then raise exception 'Der Anker in promote_employee kommt % mal vor, erwartet einmal.', v_anzahl; end if; execute replace(v_def, v_alt, v_neu); end $migration$; -- ═══ Selbstprüfung ════════════════════════════════════════════════ do $$ declare v_name text; v_def text; begin foreach v_name in array array['hire_employee', 'rehire_employee', 'transfer_employee', 'promote_employee'] loop select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = v_name and p.prokind = 'f'; if v_def not like '%planstelle_vorgemerkt(%' then raise exception '% fragt die vorgemerkten Planstellen nicht ab.', v_name; end if; if v_def not like '%require_hr_admin()%' then raise exception '% prueft die Rechte nicht.', v_name; end if; if v_def not like '%search_path%' then raise exception '% hat keinen festen search_path.', v_name; end if; end loop; -- Was an diesen Funktionen schon einmal verlorenging. Die Statusumwandlung -- im Wiedereintritt ist zweimal verschwunden; sie wird hier mitgeprüft, -- weil diese Migration die Funktion erneut erzeugt. select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'rehire_employee' and p.prokind = 'f'; if v_def not like '%end)::employment_status%' then raise exception 'rehire_employee wandelt den Status nicht um.'; end if; if v_def not like '%cornerstone_id%' then raise exception 'rehire_employee hat die Cornerstone-ID verloren.'; end if; select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f'; if v_def not like '%valid_from >= v_effective_date%' then raise exception 'promote_employee raeumt die Besetzung vom selben Tag nicht mehr ab.'; end if; if v_def not like '%target_position_id%' then raise exception 'promote_employee hat die Zielplanstelle verloren.'; end if; select pg_get_functiondef(p.oid) into v_def from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f'; if v_def not like '%valid_from >= v_effective%' then raise exception 'transfer_employee raeumt die Besetzung vom selben Tag nicht mehr ab.'; end if; raise notice 'Vorgemerkte Planstellen: alle vier Buchungswege pruefen sie.'; end $$;