Team-Urlaubs- und Abwesenheitsplaner
Von Wikiprompt, der freien Prompt-Enzyklopädie
Team-Urlaubs- und Abwesenheitsplaner Eine vollständige KI-Fähigkeit zur Planung von Teamurlaub rund um die Mindestbesetzung, mit Abdeckungsregeln, fairer Priorisierung, alternativen Vorschlägen und einem Python-Planungsskript.
Prompt-InhaltSpeichern
🌐
---
name: team-leave-and-holiday-coverage-planner
description: Plant Teamurlaub und Feiertagsabdeckung um die Mindestbesetzung herum, besonders über die Feiertage - prüft jeden Arbeitstag gegen Rollenabdeckungsregeln (eine Person füllt einen Slot), entscheidet, welche Anfragen in fairer Prioritätsreihenfolge genehmigt werden können, bietet lückenfreie Alternativtermine an, respektiert Sperrzeiten, Teilzeittage und halbe Tage, und verfolgt Kontingente, Übertrag und Spitzentage frei. Enthält einen Offline-Stdlib-Python-Planner mit --self-test.
---
# Teamurlaubs- und Feiertagsabdeckungsplaner
Sie helfen Teamleitern, Support- und Betriebsmanagern, Klinik- und Ladenbesitzern, die Dezember-Frage zu beantworten: Wer kann wann frei haben, ohne den Schreibtisch leer zu lassen, und ist das fair? Urlaubstabellen zeigen, wer weg ist, aber nicht, ob die verbleibenden Personen die Rollen tatsächlich abdecken können (ein Lead, der auch der einzige Tier-2-Ingenieur ist, kann nicht beides am selben Tag sein). Sie verwandeln das Team, die Abdeckungsregeln und die Urlaubsanträge in eine tagesweise Prüfung, entscheiden Anträge in einer festgelegten Prioritätsreihenfolge, bieten Alternativen statt eines flachen Neins an und kennzeichnen Kontingent- und Fairnessprobleme, bevor sie zu HR-Gesprächen werden.
## Dateien in dieser Fähigkeit
- `scripts/leave_cover.py` - liest ein Team-JSON (Personen, Rollen, Teilzeittage, Kontingente, Abdeckungsmindestwerte, Feiertage, Überschreibungen, Sperr- und Spitzentage) und eine Urlaubs-CSV; prüft jeden Arbeitstag mit einem Eine-Person-Ein-Slot-Matching, findet Lücken, die durch genehmigten Urlaub verursacht werden, entscheidet offene Anträge (letztes Jahr Spitze gearbeitet zuerst, dann wer zuerst kommt, mahlt zuerst), schlägt die nächsten lückenfreien Daten oder eine kürzere Reise vor und berichtet ein Kalenderraster, eine Kontingenttabelle und Befunde LV01-LV07 (nur Python 3.8+ Standardbibliothek)
- `references/coverage-rules.md` - wie Mindestwerte pro Rolle festgelegt werden, Überschreibungen für ruhige Tage, Teilzeitkräfte, halbe Tage, Bereitschaft und was das Matching tut
- `references/fair-leave-policy.md` - Prioritätsregeln, Sperrzeiten, Kontingent und Übertrag, wie man gut Nein sagt und was jeder Befundcode bedeutet
- `templates/leave-plan-brief.md` - der Plan, der an das Team gesendet wird
- `examples/support-team.json` - ein 7-köpfiges Kundensupport-Team über Weihnachten und Neujahr: Leads, Tier 2, Bereitschaft, eine Teilzeitkraft, Feiertage, geringere Abdeckung am 24. und 31. Dezember, eine Abrechnungslauf-Sperre und Übertragslimits
- `examples/support-leave.csv` - 3 genehmigte und 7 angefragte Urlaube, einschließlich eines halben Tages
- `examples/support-plan.md` - die tatsächliche Planner-Ausgabe für diese Dateien
## Arbeitsablauf
### 1. Eingaben sammeln
Fragen Sie nach: dem Zeitraum (die gesamte Feiertagsstrecke plus eine Woche auf jeder Seite abdecken), dem Team mit den Rollen und Arbeitstagen jeder Person, Kontingenten und bereits in diesem Urlaubsjahr genommenen Tagen, gesetzlichen Feiertagen, an denen das Team geschlossen ist (oder der reduzierten Abdeckung, wenn es geöffnet ist), der Mindestabdeckung pro Rolle und insgesamt, ruhigeren Tagen mit niedrigeren Mindestwerten, Sperrzeiten (Starts, Inventuren, Abrechnungsläufe, Audits) und den Spitzentagen, um die konkurriert wird. Dann der Urlaub: genehmigt, angefragt (mit Einreichungsdatum), Schulung, bekannte Eltern- oder Krankheitsurlaube. Fragen Sie, was das Team über Priorität vereinbart hat; wenn nichts, schlagen Sie "letztes Jahr die Spitze gearbeitet zuerst, dann wer zuerst kommt, mahlt zuerst" vor.
### 2. Planner ausführen
```
python3 scripts/leave_cover.py examples/support-team.json examples/support-leave.csv
python3 scripts/leave_cover.py team.json leave.csv --json
python3 scripts/leave_cover.py --self-test
```
Exit 0 = genehmigter Urlaub hält Abdeckung und niemand ist über Kontingent, 1 = genehmigter Urlaub bricht bereits Abdeckung oder überschreitet ein Kontingent, 2 = schlechte Eingabe (die Meldung nennt die Zeile). Ohne Shell den Kalender von Hand bauen: eine Zeile pro Person, eine Spalte pro Arbeitstag, dann für jeden Tag arbeitende Personen Rollenslots zuweisen, eine Person pro Slot, und Engpässe markieren.
### 3. Ergebnis lesen
- Lücken aus bereits genehmigtem Urlaub (LV01) zuerst: Sie brauchen jetzt eine Lösung (temporäre Abdeckung, ein Tausch, eine Agentur oder Bereitschaftsvereinbarung), keine neue Regel.
- Kontingentprobleme (LV02): genehmigter Urlaub über dem Kontingent benötigt HR-Bestätigung für Übertrag oder unbezahlte Tage.
- Anträge: die mit "genehmigen" markierten genehmigen; bei Konflikten und Sperrzeiten die Alternative zur Person bringen, nicht eine Ablehnung (LV04).
- Fairness (LV05) und Spitzentage frei: prüfen, dass niemand die besten Tage zwei Jahre in Folge bekommt, während jemand, der sie gearbeitet hat, abgelehnt wird.
- Fragilität: Rollen ohne Reserve (LV03) und Tage mit null Spielraum (LV07) brauchen einen benannten Backup.
- Nutze-oder-verliere-es (LV06): Personen, die Tage verlieren würden, sollten sie in ruhigen Wochen buchen.
### 4. Anpassen und erneut ausführen
Die offensichtlichen Korrekturen mit denselben Dateien versuchen: ein quergeschulter Backup (jemandem eine Rolle hinzufügen), ein niedrigerer Mindestwert an einem ruhigen Tag (Überschreibung), ein Tausch zwischen zwei Personen oder das Aufteilen einer langen Reise. Erneut ausführen, bis genehmigter Urlaub keine Lücken hat und jeder offene Antrag eine Entscheidung oder eine konkrete Alternative hat.
### 5. Liefern
`templates/leave-plan-brief.md` ausfüllen: die verwendete Regel, die Entscheidung pro Antrag mit Grund und Alternative, der Kalender, die Abdeckungsvereinbarungen für dünne Tage und die Kontingentnotizen. Mit Personen sprechen, deren Anträge nicht genehmigt sind, bevor der Plan veröffentlicht wird.
## Beispielergebnis
Im Support-Team-Beispiel bringt Omars genehmigte Reise ihn bereits 3 Tage über sein 25-Tage-Kontingent (LV02, hoch: HR muss Übertrag oder unbezahlte Tage bestätigen). Sam hat letztes Weihnachten gearbeitet, daher wird sein Antrag vom 24. Dez. bis 4. Jan. zuerst entschieden und genehmigt; Jonas (21.-23. Dez.) und Priyas halber Tag und 21.-23. Dez. werden ebenfalls genehmigt. Ades 29.-31. Dez. würde Tier 2 und die Gesamtabdeckung am 29. und 30. Dez. kurz lassen; der Planner bietet stattdessen den 16.-18. Dez. an. Lenas 30. Dez.-8. Jan. kreuzt die Abrechnungslauf-Sperre vom 5.-6. Jan., und nur der 31. Dez. kann ohne Lücke genehmigt werden. Jonas' zweite Reise braucht 5 Tage mit 4 übrig. Lena würde 10 Tage an das 5-Tage-Übertragslimit verlieren, und der 21.-23. und 29. Dez. haben überhaupt keinen Spielraum.
## Regeln
- Abdeckung nach Rolle mit einer Person pro Slot prüfen, nicht nur nach Kopfzahl.
- Eine festgelegte Prioritätsregel auf alle anwenden und die Reihenfolge zeigen; niemals danach entscheiden, wer am lautesten fragt.
- Immer ein "diesmal nicht" mit einem Alternativdatum, einer kürzeren Reise oder einem Tausch paaren.
- Krankheits-, Eltern- und anderen geschützten Urlaub als fest behandeln: Personen niemals bitten, ihn zu verschieben, und medizinische Details aus dem Plan heraushalten.
- Kontingente, Übertrag und unbezahlter Urlaub folgen dem Vertrag und lokalem Recht; für HR kennzeichnen, nicht entscheiden.
- Den Plan nur mit dem Team und Managern teilen, die ihn brauchen.
## Abdeckungsregeln
### Mindestwerte
- Einen Mindestwert pro **Rolle** festlegen (was abgedeckt sein muss: Lead/Genehmiger, Tier 2, Bereitschaft, Apotheker, Schlüsselinhaber, Gabelstaplerschein) und eine **beliebige** Gesamtzahl (genug Hände für das Volumen).
- Die Zahlen auf Nachfrage basieren, nicht auf Gewohnheit: Tickets/Anrufe/Bestellungen pro Tag geteilt durch das, was eine Person bewältigt, plus die Rollen, die gesetzlich oder vertraglich anwesend sein müssen.
- Ruhige Tage (24. und 31. Dezember, die Woche zwischen den Feiertagen in vielen B2B-Teams) rechtfertigen normalerweise einen niedrigeren Mindestwert: `coverage_overrides` für diese Daten verwenden, nicht den ganzen Zeitraum senken.
- Tage, an denen das Geschäft geschlossen ist, kommen in `holidays`: keine Abdeckung nötig, kein Kontingent belastet.
### Eine Person, ein Slot
Eine Person, die mehrere Rollen hat, füllt trotzdem nur einen Slot pro Tag. Der Planner weist Personen Slots mit einem bipartiten Matching zu, findet also die Anordnung, die funktioniert, wenn eine existiert:
- Maya (Lead, Tier 2) + Lena (Tier 1, Tier 2) können Lead 1 + Tier 2 1 abdecken, aber nicht Lead 1 + Tier 2 2.
- Engpässe werden pro Rolle gemeldet; "beliebig" kurz bedeutet zu wenige Personen insgesamt.
### Personen
- `days`: Arbeitstage von Teilzeitkräften; Urlaub an ihren Nicht-Arbeitstagen kostet kein Kontingent und beeinflusst die Abdeckung nicht.
- Halbe Tage (`half_day=yes`) kosten 0,5 Tage Kontingent; der Planner zählt die Person an diesem Tag nicht für die Abdeckung (konservativ). Wenn eine nur morgendliche Abwesenheit die Nachmittagsschicht noch abdeckt, im Brief sagen.
- Schulung, Krankheit, Eltern- und unbezahlter Urlaub reduzieren die Abdeckung wie Urlaub, zählen aber nicht gegen das Kontingent.
### Bereitschaft und Remote-Abdeckung
- Wenn jemand Bereitschaft von zu Hause abdeckt, während er im Urlaub ist, ihn nicht für diese Rolle als im Urlaub markieren; es explizit vereinbaren und gemäß Richtlinie bezahlen. Der Planner behandelt Urlaub als vollständig abwesend.
- Ein externer oder Agentur-Backup kann als Person mit nur dieser Rolle und keinem Kontingent hinzugefügt werden.
### Spielraum
- Null-Spielraum-Tage (genau das Minimum arbeitend) brechen mit einem Krankheitstag. Für jeden solchen Tag vor Beginn des Zeitraums einen Backup benennen.
- Eine Rolle mit nur Minimum + 1 Inhabern überlebt keine zwei Abwesenheiten; Querschulung im neuen Jahr planen.
## Faire Urlaubsentscheidungen
### Prioritätsregel (vor den Anträgen festlegen)
Häufige, verteidigbare Reihenfolge:
1. Bereits genehmigter Urlaub bleibt bestehen (es sei denn, die Person stimmt einer Änderung zu).
2. Geschützter Urlaub (Eltern, medizinisch, gesetzlich) ist fest.
3. Für Spitzentage: Personen, die die Spitze letztes Jahr gearbeitet haben, zuerst.
4. Dann wer zuerst kommt, mahlt zuerst nach Einreichungsdatum.
5. Gleichstände: rotieren (wer den letzten Gleichstand verlor, gewinnt diesen).
Nur-Dienstalter-Regeln für Spitzentage vermeiden; sie fühlen sich für neuere Mitarbeiter unfair an und verlieren oft gute Leute.
### Sperrzeiten
- Sie kurz halten und an ein echtes Ereignis binden (Start, Abrechnungslauf, Inventur, Audit, Schuljahresbeginn für ein Nachhilfeunternehmen). Früh ankündigen.
- Ein Antrag, der eine Sperrzeit berührt, wird nicht automatisch abgelehnt: der Planner bietet die nächsten lückenfreien Daten oder den längsten Teil vor der Sperrzeit an. Ausnahmen brauchen einen benannten Genehmiger.
### Kontingent und Übertrag
- Verbleibend = Kontingent - vor dem Zeitraum genommen - genehmigt - empfohlen.
- Genehmigter Urlaub über dem Kontingent (LV02 hoch) ist eine HR-Frage: Übertrag vom letzten Jahr, zusätzliche Tage kaufen oder unbezahlter Urlaub.
- Übertragslimits bedeuten, dass nicht gebuchte Tage am Jahresende verloren gehen (LV06): Personen anstoßen, ruhige Wochen zu buchen; in einigen Ländern kann gesetzlicher Mindesturlaub nicht einfach verfallen, also lokale Regeln prüfen.
### Gut Nein sagen
- Die Regel und die spezifische Lücke erklären ("Tier 2 wäre am 29. und 30. Dez. eine Person kurz").
- Die vom Planner gefundene Alternative anbieten oder einen Tausch mit einem benannten Kollegen oder eine kürzere Reise.
- Früh entscheiden (mindestens 4-6 Wochen vor den Feiertagen), damit Personen noch Reisen buchen können.
- Die Entscheidung und den Grund aufzeichnen; die Priorität des nächsten Jahres hängt davon ab.
### Befundcodes (leave_cover.py)
| Code | Schweregrad | Bedeutung |
|---|---|---|
| LV01 | hoch | genehmigter Urlaub lässt bereits eine Rolle oder die Gesamtzahl an einem Tag kurz |
| LV02 | hoch / mittel | genehmigter Urlaub über Kontingent / Antrag braucht mehr Tage als übrig |
| LV03 | mittel / niedrig | Rolle von nicht mehr Personen als dem Minimum gehalten / nur eine Reserve |
| LV04 | mittel | Antrag kollidiert mit Abdeckung oder einer Sperrzeit, mit der angebotenen Alternative |
| LV05 | mittel | jemand, der die Spitze letztes Jahr gearbeitet hat, wird abgelehnt, während andere Spitzentage bekommen |
| LV06 | niedrig | Tage über dem Übertragslimit gehen verloren |
| LV07 | info | Tage mit null Spielraum nach dem Plan |
## Feiertagsurlaubsplan-Vorlage
# Feiertagsurlaubsplan: <Team>, <Zeitraum>
**Abdeckungsregel:** <Lead 1, Tier 2 2 (1 am 24./31. Dez.), Bereitschaft 1, mindestens 4 Personen> **Geschlossen:** <25. Dez., 28. Dez., 1. Jan.>
**Verwendete Priorität:** <letztes Jahr die Spitze gearbeitet zuerst, dann wer zuerst kommt, mahlt zuerst>
**Sperrzeit:** <5.-6. Jan. Abrechnungslauf>
## Entscheidungen
| Person | Angefragt für | Entscheidung | Warum | Angebotene Alternative |
|---|---|---|---|---|
| <Sam> | <24. Dez. - 4. Jan.> | <genehmigt> | <letztes Weihnachten gearbeitet> | - |
| <Ade> | <29.-31. Dez.> | <nicht diese Daten> | <Tier 2 kurz 29.-30. Dez.> | <16.-18. Dez., oder Tausch mit ...> |
## Kalender
```
<Planner-Raster einfügen>
```
## Dünne Tage und Backups
| Tag | Warum dünn | Backup |
|---|---|---|
| <21.-23. Dez.> | <genau 4 arbeitend> | <Bereitschaft von zu Hause: ...> |
## Kontingentnotizen (für HR)
- <Omar: genehmigte Reise 3 Tage über Kontingent - Übertrag / unbezahlt bestätigen>
- <Lena: 15 Tage übrig, Übertragslimit 5 - Februar buchen vorschlagen>
## Nächste Schritte
- <mit Ade, Lena, Jonas vor Veröffentlichung sprechen>
- <Plan am ... veröffentlichen; Änderungen per Tauschanfrage an ...>
- <im Q1 einen zweiten Lead-Backup querschulen>
## Beispiel-Team-JSON
{
"team": "Northwind Kundensupport",
"period": "2026-12-14..2027-01-08",
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"holidays": ["2026-12-25", "2026-12-28", "2027-01-01"],
"coverage": {"lead": 1, "tier2": 2, "oncall": 1, "any": 4},
"coverage_overrides": [
{"dates": "2026-12-24", "coverage": {"tier2": 1, "any": 3}},
{"dates": "2026-12-31", "coverage": {"tier2": 1, "any": 3}}
],
"blackout": [
{"dates": "2027-01-05..2027-01-06", "reason": "Januar-Abrechnungslauf, alle Hände"}
],
"peak_days": "2026-12-21..2027-01-01",
"carryover_max": 5,
"people": [
{"name": "Maya", "roles": ["lead", "tier2"], "allowance": 25, "taken": 17, "worked_peak_last_year": true},
{"name": "Ade", "roles": ["lead", "tier2"], "allowance": 25, "taken": 19},
{"name": "Omar", "roles": ["tier2", "oncall"], "allowance": 25, "taken": 20},
{"name": "Lena", "roles": ["tier1", "tier2"], "allowance": 25, "taken": 10},
{"name": "Jonas", "roles": ["tier1", "oncall"], "allowance": 25, "taken": 18},
{"name": "Sam", "roles": ["tier1", "oncall"], "allowance": 25, "taken": 15, "worked_peak_last_year": true},
{"name": "Priya", "roles": ["tier1"], "days": ["mon", "wed", "thu"], "allowance": 15, "taken": 9}
]
}
## Beispiel-Urlaubs-CSV
person,start,end,type,status,submitted,half_day,notes
Maya,2026-12-21,2026-12-24,vacation,approved,2026-09-30,,Familienbesuch
Omar,2026-12-29,2027-01-08,vacation,approved,2026-10-02,,"Flüge gebucht, Lagos"
Lena,2026-12-14,2026-12-15,training,approved,2026-11-01,,Zendesk-Admin-Kurs
Jonas,2026-12-21,2026-12-23,vacation,requested,2026-10-20,,
Ade,2026-12-29,2026-12-31,vacation,requested,2026-10-25,,
Sam,2026-12-24,2027-01-04,vacation,requested,2026-11-05,,"Skifahren, letztes Weihnachten gearbeitet"
Lena,2026-12-30,2027-01-08,vacation,requested,2026-11-12,,
Priya,2026-12-16,2026-12-16,vacation,requested,2026-11-20,yes,Schul-Krippenspiel
Jonas,2027-01-04,2027-01-08,vacation,requested,2026-11-25,,
Priya,2026-12-21,2026-12-23,vacation,requested,2026-11-28,,
## Beispiel-Planner-Ausgabe
# Urlaubsabdeckungsplan: Northwind Kundensupport (2026-12-14 bis 2027-01-08)
Mindestabdeckung pro Arbeitstag: beliebig 4, lead 1, oncall 1, tier2 2. Feiertage (geschlossen): 25. Dez., 28. Dez., 01. Jan.
**Urteil: über Kontingent bei genehmigtem Urlaub** - 4 von 7 offenen Anträgen können ohne Lücke genehmigt werden; 3 brauchen eine Entscheidung.
## Anträge (in Entscheidungsreihenfolge: letztes Jahr Spitze gearbeitet zuerst, dann wer zuerst kommt, mahlt zuerst)
| # | Person | Daten | Arbeitstage | Typ | Eingereicht | Entscheidung | Warum | Alternative |
|---|---|---|---|---|---|---|---|---|
| 1 | Sam | Do 24. Dez. - Mo 04. Jan. | 5 | vacation | 2026-11-05 | genehmigen | - | - |
| 2 | Jonas | Mo 21. Dez. - Mi 23. Dez. | 3 | vacation | 2026-10-20 | genehmigen | - | - |
| 3 | Ade | Di 29. Dez. - Do 31. Dez. | 3 | vacation | 2026-10-25 | Konflikt | beliebig/tier2 kurz am 29. Dez., 30. Dez. | Mi 16. Dez. bis Fr 18. Dez. |
| 4 | Lena | Mi 30. Dez. - Fr 08. Jan. | 7 | vacation | 2026-11-12 | Sperrzeit | Januar-Abrechnungslauf, alle Hände (05. Jan., 06. Jan.) | auf Do 31. Dez. - Do 31. Dez. kürzen (1 Arbeitstag(e), vor der Sperrzeit zurück) |
| 5 | Priya | Mi 16. Dez. - Mi 16. Dez. (halb) | 0.5 | vacation | 2026-11-20 | genehmigen | - | - |
| 6 | Jonas | Mo 04. Jan. - Fr 08. Jan. | 5 | vacation | 2026-11-25 | über Kontingent | braucht 5 Tage, 4 übrig | - |
| 7 | Priya | Mo 21. Dez. - Mi 23. Dez. | 2 | vacation | 2026-11-28 | genehmigen | - | - |
## Kalender (genehmigt + empfohlen)
```
D J
11111222222233000000
45678123458901145678
MTWTFMTWTFMTWTFMTWTF
Maya .....VVVVHH...H.....
Ade .........HHxxxH.....
Omar .........HHVVVHVVVVV
Lena TT.......HH.xxHxxxxx
Jonas .....vvv.HH...Hxxxxx
Sam ........vHHvvvHv....
Priya .-h.-v-v.HH-..H.-..-
cover any +++++++++ +++ +++++
cover lead +++++++++ +++ +++++
cover oncall +++++++++ +++ +++++
cover tier2 +++++++++ +++ +++++
```
V/T/S/P/U/O genehmigter Urlaub/Schulung/Krankheit/Eltern/unbezahlt/anderes, Kleinbuchstaben = Antrag zur Genehmigung empfohlen, h = halber Tag, x = Antrag nicht genehmigt, H = Feiertag, - = kein Arbeitstag, + / ! = Rolle abgedeckt / kurz.
## Kontingent (Tage)
| Person | Kontingent | Vorher genommen | Hier genehmigt | Empfohlen | Übrig | Spitzentage frei |
|---|---|---|---|---|---|---|
| Maya | 25 | 17 | 4 | 0 | 4 | 4 |
| Ade | 25 | 19 | 0 | 0 | 6 | 0 |
| Omar | 25 | 20 | 8 | 0 | -3 | 3 |
| Lena | 25 | 10 | 0 | 0 | 15 | 0 |
| Jonas | 25 | 18 | 0 | 3 | 4 | 3 |
| Sam | 25 | 15 | 0 | 5 | 5 | 4 |
| Priya | 15 | 9 | 0 | 2.5 | 3.5 | 2 |
## Befunde
- **HOCH LV02** Omars genehmigter Urlaub überschreitet das Kontingent um 3 Tage (20 + 8 von 25 genommen); Übertrag oder unbezahlten Urlaub mit HR bestätigen.
- **MITTEL LV02** Jonas 04. Jan.-08. Jan.: braucht 5 Tage, 4 übrig; kürzere Reise genehmigen oder unbezahlte Tage vereinbaren
- **MITTEL LV04** Ade 29. Dez.-31. Dez.: Konflikt (beliebig/tier2 kurz am 29. Dez., 30. Dez.); Mi 16. Dez. bis Fr 18. Dez. anbieten
- **MITTEL LV04** Lena 30. Dez.-08. Jan.: Sperrzeit (Januar-Abrechnungslauf, alle Hände (05. Jan., 06. Jan.)); auf Do 31. Dez. - Do 31. Dez. kürzen anbieten (1 Arbeitstag(e), vor der Sperrzeit zurück)
- **NIEDRIG LV03** lead hat eine Reserveperson (Maya, Ade für ein Minimum von 1); zwei Abwesenheiten gleichzeitig brechen es.
- **NIEDRIG LV06** Ade wird noch 6 Tage übrig haben (Übertragslimit 5); 1 Tag(e) gehen verloren, wenn nicht gebucht.
- **NIEDRIG LV06** Lena wird noch 15 Tage übrig haben (Übertragslimit 5); 10 Tag(e) gehen verloren, wenn nicht gebucht.
- **INFO LV07** Kein Spielraum an 4 Tag(en) (21. Dez., 22. Dez., 23. Dez., 29. Dez.): ein Krankheitstag bricht die Abdeckung; einen Backup organisieren.
## Python-Planner-Skript
```python
#!/usr/bin/env python3
"""Teamurlaubs- und Feiertagsabdeckungsplaner (nur Python 3.8+ Standardbibliothek).
Prüft die Urlaubsanträge eines Teams gegen Mindestbesetzungsregeln Tag für Tag, entscheidet, welche
Anträge ohne Lücke genehmigt werden können, schlägt alternative Daten für den Rest vor und verfolgt
Kontingente und wer die Spitzenfeiertage bekommt.
Team-JSON:
{
"team": "Support Desk", "period": "2026-12-14..2027-01-08",
"workdays": ["mon", "tue", "wed", "thu", "fri"],
"holidays": ["2026-12-25", "2026-12-28", "2027-01-01"], (Büro geschlossen, keine Abdeckung nötig)
"coverage": {"lead": 1, "tier2": 2, "oncall": 1, "any": 4}, ("any" = Gesamtzahl arbeitender Personen)
"coverage_overrides": [{"dates": "2026-12-24", "coverage": {"tier2": 1, "any": 3}}],
"blackout": [{"dates": "2027-01-05..2027-01-06", "reason": "Abrechnungslauf"}],
"peak_days": "2026-12-21..2027-01-01",
"people": [
{"name": "Maya", "roles": ["lead", "tier2"], "allowance": 25, "taken": 17, "worked_peak_last_year": true},
{"name": "Priya", "roles": ["tier1"], "days": ["mon", "wed", "thu"], "allowance": 15, "taken": 9}
]
}
Jede arbeitende Person füllt EINEN Rollenslot pro Tag (ein Lead, der auch Tier 2 ist, zählt einmal), gelöst
als bipartites Matching.
Urlaubs-CSV (Header erforderlich): person,start,end,type,status,submitted,half_day,notes
type vacation (zählt gegen Kontingent) | sick | training | parental | unpaid | other
status approved | requested | declined (ignoriert)
half_day yes = halber Tag Kontingent; die Person wird an diesem Tag nicht für die Abdeckung gezählt
Verwendung:
python3 leave_cover.py team.json leave.csv [--json]
python3 leave_cover.py --self-test
Exit-Codes: 0 = genehmigter Urlaub hält Abdeckung und niemand ist über Kontingent, 1 = genehmigter Urlaub
bricht bereits Abdeckung oder überschreitet ein Kontingent, 2 = schlechte Eingabe.
"""
import argparse
import csv
import datetime as dt
import json
import sys
from collections import OrderedDict, defaultdict
DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
TYPE_CODE = {"vacation": "V", "sick": "S", "training": "T", "parental": "P", "unpaid": "U", "other": "O"}
class InputError(ValueError):
pass
def d(s, what="date"):
try:
return dt.date.fromisoformat(str(s).strip())
except ValueError:
raise InputError("%s %r ist nicht JJJJ-MM-TT" % (what, s))
def date_range(spec):
spec = str(spec)
a, _, b = spec.partition("..")
a, b = d(a), d(b or a)
if b < a:
raise InputError("Bereich %r endet vor seinem Beginn" % spec)
return [a + dt.timedelta(days=i) for i in range((b - a).days + 1)]
# ----------------------------------------------------------------------------- Eingaben
def load_team(cfg):
if "period" not in cfg or not cfg.get("people"):
raise InputError("Team-JSON braucht 'period' und 'people'")
t = dict(cfg)
t["days"] = date_range(cfg["period"])
t["workdays"] = [w.lower()[:3] for w in cfg.get("workdays", DAYS[:5])]
t["holidays"] = {d(h, "Feiertag") for h in cfg.get("holidays", [])}
t["coverage"] = {k: int(v) for k, v in (cfg.get("coverage") or {}).items()}
over = {}
for o in cfg.get("coverage_overrides", []):
for day in date_range(o["dates"]):
over[day] = {k: int(v) for k, v in o["coverage"].items()}
t["overrides"] = over
t["blackout"] = [(set(date_range(b["dates"])), b.get("reason", "Sperrzeit")) for b in cfg.get("blackout", [])]
t["peak"] = set(date_range(cfg["peak_days"])) if cfg.get("peak_days") else set()
people = OrderedDict()
for p in cfg["people"]:
if "name" not in p:
raise InputError("jede Person braucht einen Namen")
q = dict(p)
q["roles"] = list(p.get("roles", []))
q["days"] = [w.lower()[:3] for w in p.get("days", t["workdays"])]
q["allowance"] = float(p.get("allowance", 0))
q["taken"] = float(p.get("taken", 0))
q["carryover_max"] = p.get("carryover_max", cfg.get("carryover_max"))
people[p["name"]] = q
t["people"] = people
roles = {r for p in people.values() for r in p["roles"]}
for r in list(t["coverage"]) + [r for o in over.values() for r in o]:
if r != "any" and r not in roles:
raise InputError("Abdeckungsrolle %r wird von niemandem gehalten" % r)
return t
def load_leave(path, team):
out = []
with open(path, newline="", encoding="utf-8") as f:
rows = [r for r in f if r.strip() and not r.lstrip().startswith("#")]
for n, row in enumerate(csv.DictReader(rows), 2):
name = (row.get("person") or "").strip()
if name not in team["people"]:
raise InputError("Urlaubszeile %d: unbekannte Person %r" % (n, name))
typ = (row.get("type") or "vacation").strip().lower()
if typ not in TYPE_CODE:
raise InputError("Urlaubszeile %d: Typ muss einer von %s sein" % (n, ", ".join(TYPE_CODE)))
status = (row.get("status") or "requested").strip().lower()
if status not in ("approved", "requested", "declined"):
raise InputError("Urlaubszeile %d: Status muss approved, requested oder declined sein" % n)
start, end = d(row.get("start"), "Start"), d(row.get("end") or row.get("start"), "Ende")
if end < start:
raise InputError("Urlaubszeile %d: Ende vor Beginn" % n)
out.append({"id": n, "person": name, "start": start, "end": end, "type": typ, "status": status,
"submitted": (row.get("submitted") or "").strip() or "9999-12-31",
"half": (row.get("half_day") or "").strip().lower() in ("yes", "y", "true", "1"),
"notes": (row.get("notes") or "").strip()})
return out
# ----------------------------------------------------------------------------- Kern
def is_workday(team, day):
return DAYS[day.weekday()] in team["workdays"] and day not in team["holidays"]
def person_works(team, p, day):
return is_workday(team, day) and DAYS[day.weekday()] in p["days"]
def leave_days(team, lv):
p = team["people"][lv["person"]]
days = []
cur = lv["start"]
while cur <= lv["end"]:
if person_works(team, p, cur):
days.append(cur)
cur += dt.timedelta(days=1)
return days
def charge(team, lv):
n = len(leave_days(team, lv))
return 0.5 * n if lv["half"] else float(n)
def need_for(team, day):
need = dict(team["coverage"])
need.update(team["overrides"].get(day, {}))
return need
def shortfall(team, day, off):
"""Gibt {Rolle: fehlend} für einen Tag zurück, gegeben die Menge der abwesenden Personen."""
if not is_workday(team, day):
return {}
avail = [p for n, p in team["people"].items() if n not in off and person_works(team, p, day)]
need = need_for(team, day)
slots = [r for r, k in sorted(need.items()) if r != "any" for _ in range(k)]
match = {} # Slot-Index -> Personenname
def try_assign(pi, seen):
for si, role in enumerate(slots):
if role in avail[pi]["roles"] and si not in seen:
seen.add(si)
if si not in match or try_assign(match[si], seen):
match[si] = pi
return True
return False
for pi in range(len(avail)):
try_assign(pi, set())
short = defaultdict(int)
for si, role in enumerate(slots):
if si not in match:
short[role] += 1
if need.get("any", 0) > len(avail):
short["any"] = need["any"] - len(avail)
return dict(short)
def off_map(team, leaves):
off = defaultdict(set)
for lv in leaves:
for day in leave_days(team, lv):
off[day].add(lv["person"])
return off
def gaps(team, leaves):
off = off_map(team, leaves)
out = OrderedDict()
for day in team["days"]:
s = shortfall(team, day, off.get(day, set()))
if s:
out[day] = s
return out
def total(g):
return sum(sum(v.values()) for v in g.values())
def new_gaps(before, after):
out = OrderedDict()
for day, s in after.items():
b = before.get(day, {})
worse = {r: k - b.get(r, 0) for r, k in s.items() if k > b.get(r, 0)}
if worse:
out[day] = worse
return out
def blackout_hit(team, lv):
days = set(leave_days(team, lv))
for bdays, reason in team["blackout"]:
hit = sorted(days & bdays)
if hit:
return hit, reason
return None, None
def suggest(team, current, lv, max_shift=15):
"""Nächste Verschiebung desselben Antrags (gleiche Kalenderlänge), die Abdeckung hält."""
length = (lv["end"] - lv["start"]).days
want = len(leave_days(team, lv))
base = gaps(team, current)
first, last = team["days"][0], team["days"][-1]
for shift in sorted(range(-max_shift, max_shift + 1), key=lambda x: (abs(x), x)):
if shift == 0:
continue
cand = dict(lv, start=lv["start"] + dt.timedelta(days=shift), end=lv["start"] + dt.timedelta(days=shift + length))
if cand["start"] < first or cand["end"] > last:
continue
if len(leave_days(team, cand)) < want or blackout_hit(team, cand)[0]:
continue
if not new_gaps(base, gaps(team, current + [cand])):
return cand
return None
def trimmed(team, current, lv):
"""Längster Teil eines Sperrzeit-Antrags, vor dem ersten Sperrzeittag, der Abdeckung hält."""
hit, _ = blackout_hit(team, lv)
days = [x for x in leave_days(team, lv) if x < hit[0]]
base = gaps(team, current)
best = None
for i in range(len(days)):
for j in range(len(days) - 1, i - 1, -1):
if best and j - i + 1 <= len(leave_days(team, best)):
break
cand = dict(lv, start=days[i], end=days[j])
if not new_gaps(base, gaps(team, current + [cand])):
best = cand
break
return best
def plan(team, leaves):
findings = []
def add(sev, code, msg):
findings.append({"severity": sev, "code": code, "message": msg})
approved = [l for l in leaves if l["status"] == "approved"]
requests = [l for l in leaves if l["status"] == "requested"]
used = defaultdict(float)
for l in approved:
if l["type"] == "vacation":
used[l["person"]] += charge(team, l)
g_approved = gaps(team, approved)
for day, s in g_approved.items():
add("high", "LV01", "%s %s: genehmigter Urlaub lässt bereits %s kurz" % (
day.isoformat(), DAYS[day.weekday()].title(), ", ".join("%s um %d" % (r, k) for r, k in sorted(s.items()))))
def priority(l):
p = team["people"][l["person"]]
touches_peak = bool(set(leave_days(team, l)) & team["peak"])
return (0 if touches_peak and p.get("worked_peak_last_year") else 1, l["submitted"], l["id"])
current = list(approved)
decisions = []
for l in sorted(requests, key=priority):
p = team["people"][l["person"]]
days = leave_days(team, l)
c = charge(team, l) if l["type"] == "vacation" else 0.0
dec = {"leave": l, "workdays": len(days), "charge": c, "decision": "approve", "reason": "", "suggestion": None}
if not days:
dec.update(decision="keine Arbeitstage", reason="fällt auf Feiertage / nur Nicht-Arbeitstage")
elif l["type"] == "vacation" and p["allowance"] and p["taken"] + used[l["person"]] + c > p["allowance"] + 1e-9:
left = p["allowance"] - p["taken"] - used[l["person"]]
dec.update(decision="über Kontingent", reason="braucht %g Tage, %g übrig" % (c, max(left, 0)))
else:
hit, reason = blackout_hit(team, l)
if hit:
dec.update(decision="Sperrzeit", reason="%s (%s)" % (reason, ", ".join(x.strftime("%d. %b") for x in hit)))
else:
ng = new_gaps(gaps(team, current), gaps(team, current + [l]))
if ng:
roles = sorted({r for s in ng.values() for r in s})
dec.update(decision="Konflikt", reason="%s kurz am %s" % (
"/".join(roles), ", ".join(x.strftime("%d. %b") for x in ng)))
if dec["decision"] == "approve":
current.append(l)
used[l["person"]] += c
elif dec["decision"] in ("Konflikt", "Sperrzeit"):
s = suggest(team, current, l)
if s:
dec["suggestion"] = "%s bis %s" % (s["start"].strftime("%a %d. %b"), s["end"].strftime("%a %d. %b"))
elif dec["decision"] == "Sperrzeit":
t = trimmed(team, current, l)
if t:
dec["suggestion"] = "auf %s - %s kürzen (%d Arbeitstag(e), vor der Sperrzeit zurück)" % (
t["start"].strftime("%a %d. %b"), t["end"].strftime("%a %d. %b"), len(leave_days(team, t)))
decisions.append(dec)
approved_peak = defaultdict(int)
for l in current:
approved_peak[l["person"]] += len(set(leave_days(team, l)) & team["peak"])
for dec in decisions:
l = dec["leave"]
p = team["people"][l["person"]]
if dec["decision"] != "approve" and p.get("worked_peak_last_year") and set(leave_days(team, l)) & team["peak"]:
lucky = [n for n, k in approved_peak.items() if k and not team["people"][n].get("worked_peak_last_year")]
add("medium", "LV05", "%s hat die Spitze letztes Jahr gearbeitet, aber dieser Antrag ist %s, während %s Spitzentage frei bekommen; "
"Tausch in Betracht ziehen." % (l["person"], dec["decision"], ", ".join(sorted(lucky)) or "niemand"))
g_final = gaps(team, current)
allowance = []
for n, p in team["people"].items():
planned_req = sum(dec["charge"] for dec in decisions if dec["leave"]["person"] == n and dec["decision"] == "approve")
appr = used[n] - planned_req
left = p["allowance"] - p["taken"] - used[n] if p["allowance"] else None
allowance.append({"person": n, "allowance": p["allowance"], "taken_before": p["taken"], "approved": appr,
"recommended": planned_req, "left": left, "peak_days_off": approved_peak.get(n, 0)})
if p["allowance"] and p["taken"] + appr > p["allowance"] + 1e-9:
add("high", "LV02", "%ss genehmigter Urlaub überschreitet das Kontingent um %g Tage (%g + %g von %g genommen); "
"Übertrag oder unbezahlten Urlaub mit HR bestätigen." % (n, p["taken"] + appr - p["allowance"], p["taken"],
appr, p["allowance"]))
cm = p.get("carryover_max")
if left is not None and cm is not None and left > float(cm):
add("low", "LV06", "%s wird noch %g Tage übrig haben (Übertragslimit %g); %g Tag(e) gehen verloren, wenn nicht gebucht."
% (n, left, float(cm), left - float(cm)))
for role, k in sorted(team["coverage"].items()):
if role == "any":
continue
holders = [n for n, p in team["people"].items() if role in p["roles"]]
if len(holders) <= k:
add("medium", "LV03", "Nur %d Personen können %s abdecken (Minimum %d): jede Abwesenheit von %s bricht die Abdeckung; "
"einen Backup querschulen." % (len(holders), role, k, ", ".join(holders)))
elif len(holders) == k + 1:
add("low", "LV03", "%s hat eine Reserveperson (%s für ein Minimum von %d); zwei Abwesenheiten gleichzeitig brechen es."
% (role, ", ".join(holders), k))
for day, s in g_final.items():
if day not in g_approved:
add("high", "LV01", "%s nach Entscheidungen immer noch kurz: %s" % (day, s))
for dec in decisions:
if dec["decision"] in ("Konflikt", "Sperrzeit"):
l = dec["leave"]
add("medium", "LV04", "%s %s-%s: %s (%s)%s" % (
l["person"], l["start"].strftime("%d. %b"), l["end"].strftime("%d. %b"), dec["decision"], dec["reason"],
("; " + dec["suggestion"] + " anbieten") if dec["suggestion"] else "; keine lückenfreie Alternative innerhalb von 15 Tagen"))
elif dec["decision"] == "über Kontingent":
l = dec["leave"]
add("medium", "LV02", "%s %s-%s: %s; kürzere Reise genehmigen oder unbezahlte Tage vereinbaren" % (
l["person"], l["start"].strftime("%d. %b"), l["end"].strftime("%d. %b"), dec["reason"]))
thin = []
off = off_map(team, current)
for day in team["days"]:
if not is_workday(team, day):
continue
working = [n for n, p in team["people"].items() if n not in off.get(day, set()) and person_works(team, p, day)]
if len(working) == need_for(team, day).get("any", 0):
thin.append(day)
if thin:
add("info", "LV07", "Kein Spielraum an %d Tag(en) (%s): ein Krankheitstag bricht die Abdeckung; einen Backup organisieren." % (
len(thin), ", ".join(x.strftime("%d. %b") for x in thin)))
order = {"high": 0, "medium": 1, "low": 2, "info": 3}
findings.sort(key=lambda f: (order[f["severity"]], f["code"]))
return {"team": team.get("team", "Team"), "period": team["period"], "gaps_approved": g_approved,
"gaps_final": g_final, "decisions": decisions, "allowance": allowance, "findings": findings,
"current": current, "thin": thin}
# ----------------------------------------------------------------------------- Bericht
def grid(team, leaves_final, all_leaves):
days = [x for x in team["days"] if x.weekday() < 5 or DAYS[x.weekday()] in team["workdays"]]
final_ids = {l["id"] for l in leaves_final}
cell = defaultdict(dict)
for l in all_leaves:
if l["status"] == "declined":
continue
for day in leave_days(team, l):
if l["id"] in final_ids:
c = TYPE_CODE[l["type"]] if l["status"] == "approved" else TYPE_CODE[l["type"]].lower()
else:
c = "x"
if l["half"]:
c = "h" if c != "x" else "x"
cell[l["person"]][day] = c
width = max([len(n) for n in team["people"]] + [len("cover " + r) for r in team["coverage"]]) + 2
months = " " * width + "".join(x.strftime("%b")[0] if i == 0 or x.month != days[i - 1].month
else " " for i, x in enumerate(days))
L = [months, " " * width + "".join(str(x.day // 10) for x in days),
" " * width + "".join(str(x.day % 10) for x in days),
" " * width + "".join(DAYS[x.weekday()][0].upper() for x in days)]
for n, p in team["people"].items():
row = []
for x in days:
if x in team["holidays"]:
row.append("H")
elif not person_works(team, p, x):
row.append("-")
else:
row.append(cell[n].get(x, "."))
L.append(n.ljust(width) + "".join(row))
off = off_map(team, leaves_final)
for role in sorted(team["coverage"]):
row = []
for x in days:
if not is_workday(team, x):
row.append(" ")
continue
s = shortfall(team, x, off.get(x, set()))
row.append("!" if role in s else "+")
L.append(("cover " + role).ljust(width) + "".join(row))
return L
def report(team, leaves, r):
L = ["# Urlaubsabdeckungsplan: %s (%s)" % (r["team"], r["period"].replace("..", " bis ")), ""]
need = ", ".join("%s %d" % (k, v) for k, v in sorted(team["coverage"].items()))
L.append("Mindestabdeckung pro Arbeitstag: %s. Feiertage (geschlossen): %s." % (
need, ", ".join(x.strftime("%d. %b") for x in sorted(team["holidays"])) or "keine"))
n_appr = sum(1 for dec in r["decisions"] if dec["decision"] == "approve")
verdict = ("genehmigter Urlaub bricht bereits Abdeckung" if r["gaps_approved"] else
"über Kontingent bei genehmigtem Urlaub" if any(f["code"] == "LV02" and f["severity"] == "high" for f in r["findings"])
else "abgedeckt")
L += ["", "**Urteil: %s** - %d von %d offenen Anträgen können ohne Lücke genehmigt werden; %d brauchen eine Entscheidung." % (
verdict, n_appr, len(r["decisions"]), len(r["decisions"]) - n_appr), ""]
L += ["## Anträge (in Entscheidungsreihenfolge: letztes Jahr Spitze gearbeitet zuerst, dann wer zuerst kommt, mahlt zuerst)", "",
"| # | Person | Daten | Arbeitstage | Typ | Eingereicht | Entscheidung | Warum | Alternative |",
"|---|---|---|---|---|---|---|---|---|"]
for i, dec in enumerate(r["decisions"], 1):
l = dec["leave"]
L.append("| %d | %s | %s - %s%s | %s | %s | %s | %s | %s | %s |" % (
i, l["person"], l["start"].strftime("%a %d. %b"), l["end"].strftime("%a %d. %b"), " (halb)" if l["half"] else "",
("%g" % (0.5 * dec["workdays"])) if l["half"] else dec["workdays"], l["type"], l["submitted"][:10],
dec["decision"], dec["reason"] or "-", dec["suggestion"] or "-"))
L += ["", "## Kalender (genehmigt + empfohlen)", "", "```"] + grid(team, r["current"], leaves) + [
"```", "V/T/S/P/U/O genehmigter Urlaub/Schulung/Krankheit/Eltern/unbezahlt/anderes, Kleinbuchstaben = Antrag zur "
"Genehmigung empfohlen, h = halber Tag, x = Antrag nicht genehmigt, H = Feiertag, - = kein Arbeitstag, "
"+ / ! = Rolle abgedeckt / kurz.", ""]
L += ["## Kontingent (Tage)", "", "| Person | Kontingent | Vorher genommen | Hier genehmigt | Empfohlen | Übrig | Spitzentage frei |",
"|---|---|---|---|---|---|---|"]
for a in r["allowance"]:
L.append("| %s | %g | %g | %g | %g | %s | %d |" % (a["person"], a["allowance"], a["taken_before"], a["approved"],
a["recommended"], "-" if a["left"] is None else "%g" % a["left"],
a["peak_days_off"]))
L += ["", "## Befunde", ""]
if not r["findings"]:
L.append("- keine")
for f in r["findings"]:
L.append("- **%s %s** %s" % (f["severity"].upper(), f["code"], f["message"]))
return "\n".join(L) + "\n"
# ----------------------------------------------------------------------------- Selbsttest
def self_test():
import os
import tempfile
cases = []
def ok(name, cond, detail=""):
cases.append((name, bool(cond), detail))
base = {"team": "T", "period": "2026-12-14..2026-12-25", "holidays": ["2026-12-25"], "peak_days": "2026-12-21..2026-12-24",
"coverage": {"lead": 1, "dev": 1, "any": 2},
"people": [{"name": "A", "roles": ["lead", "dev"], "allowance": 10, "taken": 0},
{"name": "B", "roles": ["dev"], "allowance": 10, "taken": 9},
{"name": "C", "roles": ["lead"], "allowance": 10, "taken": 0, "worked_peak_last_year": True},
{"name": "D", "roles": ["dev"], "days": ["mon", "tue"], "allowance": 5, "taken": 0}]}
t = load_team(base)
ok("Wochenenden und Feiertage sind keine Arbeitstage", not is_workday(t, d("2026-12-19")) and not is_workday(t, d("2026-12-25")))
sf = shortfall(t, d("2026-12-16"), {"B", "C", "D"})
ok("eine Person füllt einen Slot (A kann nicht Lead und Dev sein)", sf.get("any") == 1 and sf.get("lead", 0) + sf.get("dev", 0) == 1, sf)
ok("Matching findet A=dev, C=lead", shortfall(t, d("2026-12-16"), {"B", "D"}) == {})
ok("Teilzeitkraft zählt nur an eigenen Tagen", shortfall(t, d("2026-12-17"), {"A", "B"}) == {"dev": 1, "any": 1}, shortfall(t, d("2026-12-17"), {"A", "B"}))
lv = {"id": 1, "person": "D", "start": d("2026-12-14"), "end": d("2026-12-20"), "type": "vacation",
"status": "requested", "submitted": "2026-11-01", "half": False, "notes": ""}
ok("Urlaubstage überspringen Nicht-Arbeitstage", len(leave_days(t, lv)) == 2)
ok("halber Tag belastet 0.5", charge(t, dict(lv, half=True)) == 1.0)
d0 = tempfile.mkdtemp()
csvp = os.path.join(d0, "l.csv")
with open(csvp, "w") as f:
f.write("person,start,end,type,status,submitted,half_day,notes\n"
"A,2026-12-21,2026-12-22,vacation,approved,2026-10-01,,\n"
"C,2026-12-21,2026-12-21,vacation,requested,2026-11-10,,\n"
"B,2026-12-14,2026-12-16,vacation,requested,2026-10-05,,\n"
"D,2026-12-15,2026-12-15,training,requested,2026-11-01,,\n")
leaves = load_leave(csvp, t)
r = plan(t, leaves)
by = {(dec["leave"]["person"]): dec for dec in r["decisions"]}
ok("C kollidiert mit As genehmigtem Urlaub (kein Lead)", by["C"]["decision"] == "Konflikt", by["C"])
ok("Konflikt bekommt ein Alternativdatum", by["C"]["suggestion"] is not None, by["C"])
ok("B über Kontingent (3 Tage, 1 übrig)", by["B"]["decision"] == "über Kontingent", by["B"])
ok("D Schulung genehmigt", by["D"]["decision"] == "approve", by["D"])
ok("Spitze-gearbeitet-Person zuerst entschieden", r["decisions"][0]["leave"]["person"] == "C", [x["leave"]["person"] for x in r["decisions"]])
codes = {f["code"] for f in r["findings"]}
ok("LV03 Single Point of Failure für Lead", "LV03" in codes, codes)
ok("LV05 Fairness-Flag für C", "LV05" in codes, codes)
ok("keine Lücken aus genehmigtem Urlaub", not r["gaps_approved"])
with open(csvp, "a") as f:
f.write("C,2026-12-21,2026-12-21,sick,approved,2026-12-01,,\n")
r2 = plan(t, load_leave(csvp, t))
ok("LV01 Lücke im genehmigten Urlaub", any(f["code"] == "LV01" and f["severity"] == "high" for f in r2["findings"]))
t2 = load_team(dict(base, blackout=[{"dates": "2026-12-16", "reason": "Release"}]))
r3 = plan(t2, load_leave(csvp, t2))
ok("Sperrzeit vor Abdeckung", any(x["decision"] == "Sperrzeit" for x in r3["decisions"]) or
all(x["leave"]["person"] != "B" or x["decision"] == "über Kontingent" for x in r3["decisions"]))
lvb = dict(lv, person="A", start=d("2026-12-16"), end=d("2026-12-16"))
ok("blackout_hit erkennt Überlappung", blackout_hit(t2, lvb)[0] == [d("2026-12-16")])
try:
load_team(dict(base, coverage={"qa": 1}))
ok("unbekannte Abdeckungsrolle abgelehnt", False)
except InputError:
ok("unbekannte Abdeckungsrolle abgelehnt", True)
rep = report(t, leaves, r)
ok("Bericht hat Raster und Urteil", "Urteil" in rep and "cover lead" in rep)
for name, good, detail in cases:
print("%s %s%s" % ("PASS" if good else "FAIL", name, "" if good else " -> %r" % (detail,)))
passed = sum(1 for _, g, _ in cases if g)
print("%d/%d bestanden" % (passed, len(cases)))
return 0 if passed == len(cases) else 1
def to_json(r):
def conv(o):
if isinstance(o, dt.date):
return o.isoformat()
if isinstance(o, dict):
return {(k.isoformat() if isinstance(k, dt.date) else k): conv(v) for k, v in o.items()}
if isinstance(o, (list, tuple)):
return [conv(x) for x in o]
return o
return json.dumps(conv(r), indent=2)
def main(argv=None):
ap = argparse.ArgumentParser(description="Teamurlaub gegen Mindestabdeckung planen.")
ap.add_argument("team", nargs="?")
ap.add_argument("leave", nargs="?")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not (a.team and a.leave):
ap.print_usage(sys.stderr)
return 2
try:
with open(a.team, encoding="utf-8") as f:
team = load_team(json.load(f))
leaves = load_leave(a.leave, team)
r = plan(team, leaves)
except (InputError, OSError, json.JSONDecodeError, KeyError) as e:
print("Fehler: %s" % e, file=sys.stderr)
return 2
print(to_json(r) if a.json else report(team, leaves, r), end="\n" if a.json else "")
bad = r["gaps_approved"] or any(f["code"] == "LV02" and f["severity"] == "high" for f in r["findings"])
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
```
Melde dich an, um den vollständigen Prompt zu sehen
Weiter mit:
Mit der Anmeldung akzeptierst du unsere Nutzungsbedingungen und Datenschutz
Verwendung
Dieser Prompt ist für die Verwendung mit business gedacht. Kopiere den Inhalt oben und füge ihn in dein bevorzugtes KI-Tool ein.
Für beste Ergebnisse passe die Platzhalter (eckige Klammern oder Großbuchstaben) an deine Anforderungen an.
Diskussion
0 Kommentare