Vorgemerkte Planstellen, Stichtag fuer Einheiten, Kuendigungsschutz und Elternteilzeit
F.24 -- eine Planstelle, auf die eine Versetzung vorgemerkt ist, galt als frei. Die Vormerkung steht in pending_org_changes, "besetzt?" wurde allein an position_assignments gefragt. Auffallen wuerde es erst in der Nacht des Wirksamkeitstages: dort legt der Nachtlauf die zweite Besetzung an und laeuft in den Teilindex, der genau eine laufende Besetzung je Planstelle zulaesst. Er arbeitet in einer Transaktion ueber alle faelligen Vorgaenge -- eine einzige solche Buchung haette ihn vollstaendig zum Stehen gebracht, auch fuer alle anderen. Neue Auskunft planstelle_vorgemerkt, abgefragt in hire_employee, rehire_employee, transfer_employee und promote_employee, mit Datum in der Meldung. Die eigene Vormerkung zaehlt nicht als Hindernis. D.06 -- die Struktursicht zeigte Einheiten, die es am gewaehlten Stichtag noch nicht gab: org_units wurde ungefiltert gelesen, waehrend Planstellen und Besetzungen daneben auf den Stichtag eingeschraenkt waren. Die Druckansicht filterte schon immer richtig; jetzt tun es alle drei Stellen. H.09 -- der Kuendigungsschutz stand in der Akte nur als "bis TT.MM.JJJJ", obwohl Personenkreis, Beginn, Ende und die ganze Begunstigung erfasst werden. Der Personenkreis ist die eigentliche Auskunft. Die Behinderung bekommt eine eigene Zeile: sie fuehrt oft zu Kuendigungsschutz, ist aber ein eigener Bescheid. Elternteilzeit und Wiedereingliederungsteilzeit sind jetzt auch bei der Stundenaenderung waehlbar. Sie standen nur bei der Rueckkehr aus einer Abwesenheit, mit der Begruendung, dass sie typischerweise dann beginnen -- typischerweise ist aber nicht immer. Die Ueberschneidung der beiden Listen ist damit gewollt; der Test prueft nicht mehr auf Partition, sondern darauf, dass keine Teilzeit an keinem der beiden Wege haengt.
This commit is contained in:
240
db/migrations/20260929200000_vorgemerkte_planstelle.sql
Normal file
240
db/migrations/20260929200000_vorgemerkte_planstelle.sql
Normal file
@@ -0,0 +1,240 @@
|
||||
-- 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
|
||||
$$;
|
||||
Reference in New Issue
Block a user