Initial commit: NRW Dienstplan Generator (Variante 2)

This commit is contained in:
Kenearos 2025-11-14 14:43:34 +01:00
commit 99480bb7ff
10 changed files with 960 additions and 0 deletions

21
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,21 @@
Python project for creating Excel xlsx files with openpyxl library.
## Project Structure
- `src/main.py` - Main script for creating Excel files
- `output/` - Directory for generated Excel files
- `requirements.txt` - Python dependencies (openpyxl)
- `.venv/` - Python virtual environment
## Setup Instructions
1. Virtual environment is already configured
2. Dependencies are already installed
3. Run the script: `python src/main.py`
## Usage
The main script demonstrates how to:
- Create Excel workbooks
- Add and format headers
- Insert data rows
- Apply styling (fonts, colors, alignment)
- Adjust column widths
- Save files with timestamps

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# Python
.venv/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
# Excel
output/*.xlsx
templates/*.xlsx
~$*.xlsx
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

65
README.md Normal file
View file

@ -0,0 +1,65 @@
# Excel XLSX Generator
Ein Python-Projekt zum Erstellen von Excel-Dateien (.xlsx) mit der openpyxl-Bibliothek.
## Voraussetzungen
- Python 3.8 oder höher
- pip (Python Package Installer)
## Installation
1. Erstellen Sie eine virtuelle Umgebung (empfohlen):
```powershell
python -m venv venv
```
1. Aktivieren Sie die virtuelle Umgebung:
```powershell
.\venv\Scripts\Activate.ps1
```
1. Installieren Sie die erforderlichen Pakete:
```powershell
pip install -r requirements.txt
```
## Verwendung
Führen Sie das Hauptskript aus:
```powershell
python src/main.py
```
Dies erstellt eine Excel-Datei `output/example.xlsx` mit Beispieldaten.
### NRW-Dienstplan-Vorlage erstellen
Das Skript `src/build_template.py` erzeugt eine leere Excel-Vorlage mit allen Regeln für NRW (Wochenenddefinition FrSo, Feiertage + Vortag, automatische Abzüge).
```powershell
python src/build_template.py
```
Die Vorlage wird unter `templates/Dienstplan_Template_NRW.xlsx` abgelegt. Dort tragen Sie lediglich Namen/Anteile ein; die Abrechnung erfolgt über die vorbereiteten Formeln.
## Projektstruktur
```text
.
├── src/
│ ├── main.py # Beispielskript für XLSX-Ausgabe
│ └── build_template.py # Generator für die NRW-Dienstplan-Vorlage
├── output/ # Ausgabeverzeichnis für erstellte Excel-Dateien
├── templates/ # Enthält die generierte Dienstplan-Vorlage
├── requirements.txt # Python-Abhängigkeiten
└── README.md # Diese Datei
```
## Anpassung
Bearbeiten Sie `src/main.py`, um Ihre eigenen Excel-Dateien zu erstellen.

328
SPECIFICATION.md Normal file
View file

@ -0,0 +1,328 @@
# README.txt — Monatsplan mit automatischer Vergütung (Variante 2 „streng")
Stand: 14.11.2025 (Deutschland)
## Ziel
Diese README beschreibt vollständig, wie eine Excel-Arbeitsmappe aufgebaut wird, die Monatsdienste erfasst und automatisch die Vergütung ermittelt inkl. Erkennung von Wochenend-/Feiertagsdiensten (inkl. Vortag), Schwellenlogik und Abzug 1,0 WE-Einheit. Variante 2 (streng) ist aktiv: WE-Dienste werden nur vergütet, wenn im Monat mindestens 2,0 WE-Einheiten erreicht werden; sonst 0 €. Wochentage (kein WE) werden stets vergütet.
Hinweise:
- Region: Deutschland, Bundesland wählbar (steuert Feiertage).
- Excel-Region: deutsches Excel (Funktionsargumente „;", Dezimal „,").
- Monatsbezug: Regeln!Monat_Auswahl (erster Tag des Monats).
## Fachliche Regeln (Single Source of Truth)
### Begriffe
- **WE-Tag** (Wochenend-/Feiertagsdienst) ist jeder:
- Freitag, Samstag, Sonntag
- gesetzlicher Feiertag des gewählten Bundeslands
- der Tag vor einem gesetzlichen Feiertag (Vortag)
- **WT-Tag** (Wochentag): jeder Tag, der kein WE-Tag ist.
### Vergütung
- **WT** (kein WE-Tag): 250 € pro 1,0 Einheit (Splits anteilig).
- **WE** (WE-Tag):
- Wenn Monats-Summe WE-Einheiten je Person < 2,0 Auszahlung 0 für alle WE-Einheiten.
- Wenn Monats-Summe WE-Einheiten ≥ 2,0 → Auszahlung 450 €/WE-Einheit,
anschließend Abzug genau 1,0 WE-Einheit (max. 1× pro Person/Monat).
- Abzugs-Priorität: zuerst aus Freitag-WE-Einheiten, Rest aus den übrigen WE-Einheiten (Sa/So/Feiertag/Vortag). Chronologie muss nicht nachgebildet werden; es genügt die Priorität nach Kategorie.
### Splits/Anteile
- Pro Dienst Eintrag mit Anteil in (0,0 … 1,0]; mehrere Einträge pro Datum möglich.
- Summe der Anteile je Datum soll 1,0 sein (Ampel-/Plausibilitätscheck).
### Grenzen und Klarstellungen
- Schwelle gilt je Person und Kalendermonat.
- Abzug wird nur angewandt, wenn Schwelle erreicht (≥ 2,0).
- WE-Dienste unterhalb der Schwelle werden NICHT als Wochentage vergütet.
- Rundung: Bei Schwellenprüfung Toleranz 1e-4 (z. B. 1,99995 ≈ 2,0).
## Parameter (Blatt „Regeln")
- Satz_WT = 250
- Satz_WE = 450
- WE_Schwelle = 2,0
- Abzug_nach_WE_Schwelle = 1,0
- BL_Auswahl = Dropdown (z. B. BW, BY, BE, …)
- Monat_Auswahl = Datum (erster Tag des Zielmonats, z. B. 01.11.2025)
- Variante = 2 (fix auf „streng")
Optional dokumentieren:
- Version, Autor, Änderungsdatum, Kurzregeln.
## Datei-/Blattstruktur
### 1) Regeln
- Parameter s. oben
- Kurzbeschreibung (Was/Wie/Stand)
### 2) Feiertage
Tabelle „tblFeiertage" mit Spalten:
- Datum (Datum)
- Name (Text)
- BL (Text, Kürzel des Bundeslandes)
Beispiel-CSV-Schema: Datum;Name;BL
### 3) Plan (Erfassung)
Tabelle „tblPlan" (Eingabe durch Nutzer):
- Datum (Datum, Pflicht)
- Mitarbeiter (Text, Pflicht)
- Anteil (Zahl, 0<Anteil1; Summe je Datum 1,0)
Hilfsspalten (Formeln, verborgen oder am Rand):
- Ist_FEIERTAG, Ist_VORTAG, Ist_Freitag, Ist_WE_Tag, Ist_WT_Tag
- MonatKey (YYYYMM-Marker für Monat_Auswahl)
### 4) Auswertung (je Person, je Monat)
Tabelle „tblAuswertung":
- Mitarbeiter
- WT_Einheiten
- WE_Freitag
- WE_Andere
- WE_Gesamt
- WE_Schwelle_erreicht (JA/NEIN)
- Abzug_gesamt, Abzug_von_Freitag, Abzug_von_Andere
- WE_bezahlt
- Auszahlung_WT, Auszahlung_WE, Auszahlung_Gesamt
### 5) Checks (Qualität/Prüfungen)
- Ampel „Summe Anteile je Datum = 1,0"
- Liste Unstimmigkeiten (z. B. fehlender Mitarbeiter, Datum außerhalb Monat)
## Benannte Bereiche (empfohlen)
- Regeln!Satz_WT, Regeln!Satz_WE, Regeln!WE_Schwelle, Regeln!Abzug_nach_WE_Schwelle
- Regeln!BL_Auswahl, Regeln!Monat_Auswahl, Regeln!Variante
- Feiertage_Termine (dynamisch gefilterte Feiertage des gewählten BL)
## Formeln (deutsches Excel)
Hinweis: Funktionsargumente mit „;". Für Office 365 werden FILTER/LET/XLOOKUP verwendet. Für Nicht365 stehen SUMMENPRODUKTAlternativen weiter unten.
### A) Feiertage_Termine (benannter Bereich, Office 365)
In Namen-Manager:
```
=FILTER(tblFeiertage[Datum];tblFeiertage[BL]=Regeln!BL_Auswahl)
```
### B) Erkennung im Blatt Plan (je Zeile der tblPlan)
1. MonatKey (hilft beim Filtern auf den Zielmonat):
```
=TEXT([@Datum];"YYYYMM")
```
2. Ist_FEIERTAG:
```
=ISTZAHL(VERGLEICH([@Datum];Feiertage_Termine;0))
```
3. Ist_VORTAG (Tag vor einem Feiertag):
```
=ISTZAHL(VERGLEICH([@Datum]+1;Feiertage_Termine;0))
```
4. Ist_Freitag:
```
=WOCHENTAG([@Datum];2)=5
```
5. Ist_WE_Tag:
```
=ODER([@Ist_Freitag];WOCHENTAG([@Datum];2)=6;WOCHENTAG([@Datum];2)=7;[@Ist_FEIERTAG];[@Ist_VORTAG])
```
6. Ist_WT_Tag:
```
=NICHT([@Ist_WE_Tag])
```
### C) Aggregation je Mitarbeiter im Blatt Auswertung (Office 365)
Voraussetzungen:
- In tblAuswertung steht in [@Mitarbeiter] der Name.
- Monat_Auswahl ist der 1. des Zielmonats.
- StartMonat = Regeln!Monat_Auswahl
- EndeMonat = EOMONAT(Regeln!Monat_Auswahl;0)
Für bessere Lesbarkeit hier als Zeilenformeln mit SUMMEWENNS + Filterkriterien:
1. WT_Einheiten:
```
=SUMMEWENNS(tblPlan[Anteil];
tblPlan[Mitarbeiter];[@Mitarbeiter];
tblPlan[Datum];">="&Regeln!Monat_Auswahl;
tblPlan[Datum];"<="&EOMONAT(Regeln!Monat_Auswahl;0);
tblPlan[Ist_WT_Tag];WAHR)
```
2. WE_Freitag:
```
=SUMMEWENNS(tblPlan[Anteil];
tblPlan[Mitarbeiter];[@Mitarbeiter];
tblPlan[Datum];">="&Regeln!Monat_Auswahl;
tblPlan[Datum];"<="&EOMONAT(Regeln!Monat_Auswahl;0);
tblPlan[Ist_WE_Tag];WAHR;
tblPlan[Ist_Freitag];WAHR)
```
3. WE_Andere (WE außer Freitag):
```
=SUMMEWENNS(tblPlan[Anteil];
tblPlan[Mitarbeiter];[@Mitarbeiter];
tblPlan[Datum];">="&Regeln!Monat_Auswahl;
tblPlan[Datum];"<="&EOMONAT(Regeln!Monat_Auswahl;0);
tblPlan[Ist_WE_Tag];WAHR;
tblPlan[Ist_Freitag];FALSCH)
```
4. WE_Gesamt:
```
=[@WE_Freitag]+[@WE_Andere]
```
5. Abzug_gesamt:
```
=WENN([@WE_Gesamt]>=Regeln!WE_Schwelle-0,0001;Regeln!Abzug_nach_WE_Schwelle;0)
```
6. Abzug_von_Freitag:
```
=MIN([@Abzug_gesamt];[@WE_Freitag])
```
7. Abzug_von_Andere:
```
=MAX(0;[@Abzug_gesamt]-[@Abzug_von_Freitag])
```
8. WE_bezahlt (Gate durch Schwelle):
```
=WENN([@WE_Gesamt]<Regeln!WE_Schwelle-0,0001;0;
([@WE_Freitag]-[@Abzug_von_Freitag]) + ([@WE_Andere]-[@Abzug_von_Andere]))
```
9. Auszahlung_WE:
```
=[@WE_bezahlt]*Regeln!Satz_WE
```
10. Auszahlung_WT:
```
=[@WT_Einheiten]*Regeln!Satz_WT
```
11. Auszahlung_Gesamt:
```
=[@Auszahlung_WE]+[@Auszahlung_WT]
```
12. WE_Schwelle_erreicht (JA/NEIN):
```
=WENN([@WE_Gesamt]>=Regeln!WE_Schwelle-0,0001;"JA";"NEIN")
```
### D) Nicht365Alternativen (ohne FILTER)
1. Ist_FEIERTAG (im Plan):
```
=SUMMENPRODUKT((tblFeiertage[Datum]=[@Datum])*(tblFeiertage[BL]=Regeln!BL_Auswahl))>0
```
2. Ist_VORTAG:
```
=SUMMENPRODUKT((tblFeiertage[Datum]=[@Datum]+1)*(tblFeiertage[BL]=Regeln!BL_Auswahl))>0
```
Die übrigen Aggregationen lassen sich mit SUMMENPRODUKT statt SUMMEWENNS abbilden, z. B. WT_Einheiten:
```
=SUMMENPRODUKT((tblPlan[Mitarbeiter]=[@Mitarbeiter])*
(tblPlan[Datum]>=Regeln!Monat_Auswahl)*
(tblPlan[Datum]<=EOMONAT(Regeln!Monat_Auswahl;0))*
(tblPlan[Ist_WT_Tag]=WAHR)*
(tblPlan[Anteil]))
```
## Eingabe- und Validierungsregeln
### Plan-Eingabe (tblPlan)
- Erforderlich: Datum, Mitarbeiter, Anteil (0<Anteil1).
- Pro Datum muss Summe der Anteile ≈ 1,0 sein.
### Ampel (Checks-Blatt oder bedingte Formatierung im Plan)
- Regel: Für jedes Datum D gilt |SUMME(Anteil an D) 1,0| ≤ 0,0001 → grün; sonst rot.
Beispiel-Formel (als hilfsweise Matrix in Checks):
```
=ABS(SUMMEWENNS(tblPlan[Anteil];tblPlan[Datum];D2)-1)<=0,0001
```
### Fehlerliste
- Datensätze außerhalb des ausgewählten Monats
- Anteil ≤ 0 oder > 1
- Leerer Mitarbeiter
- Doppelte Einträge, wenn nicht beabsichtigt
## Testfälle (sollten „grün" durchlaufen)
1) **Unter Schwelle**:
A hat 1,75 WE und 1,0 WT → Auszahlung_WE = 0 €; Auszahlung_WT = 250 €.
2) **Genau Schwelle**:
A hat 2,0 WE (Fr 1,0 + Sa 1,0) → Abzug 1,0 (zuerst Fr) → WE_bezahlt = 1,0 → 450 €.
3) **Über Schwelle ohne Freitag**:
A hat 2,0 WE (nur Sa+So) → Abzug 1,0 aus „Andere" → WE_bezahlt = 1,0 → 450 €.
4) **Starke Überdeckung**:
A hat 3,5 WE → Abzug 1,0 → WE_bezahlt = 2,5 → 2,5×450 €.
5) **Splits rund um 2,0**:
A hat Fr 0,4 + Sa 0,6 + So 1,0 → Summe 2,0 → Abzug 1,0
(0,4 von Fr, 0,6 von Andere) → WE_bezahlt = 1,0 → 450 €.
6) **Unter Schwelle, nur WE-Tage**:
A hat 1,0 WE, 0 WT → Auszahlung_WE = 0 €; Auszahlung_Gesamt = 0 €.
7) **Vortag-Feiertag**:
Feiertag Dienstag; Montag ist Vortag (WE). A: Mo(Vortag) 1,0 + Mi (WT) 1,0.
WE_Gesamt = 1,0 < 2,0 Auszahlung_WE = 0 ; WT = 250 .
## Edge-Cases und Präzisierungen
- Abzug nur einmal pro Person/Monat (fix 1,0), und nur wenn Schwelle erreicht.
- Der Vortag eines Feiertags ist WE-Tag unabhängig davon, welcher Wochentag er ist.
- Wenn WE_Freitag < 1,0, wird der restliche Abzug (bis 1,0) von WE_Andere genommen.
- Monatswechsel: Daten genau per >=Monat_Auswahl und <=EOMONAT(Monat_Auswahl;0) filtern.
- Rundungstoleranz 1e-4 bei Schwelle und Datumssummen (Splits wie 0,33/0,67).
- Tabellen-Namen („tblPlan", „tblFeiertage", „tblAuswertung") konsequent verwenden.
## Pflege und Handover
- Bundesland wählen: Regeln!BL_Auswahl.
- Feiertage pflegen: In „tblFeiertage" neue Jahre ergänzen (Datum/Name/BL). Keine Formeländerung nötig.
- Sätze/Schwelle/Abzug anpassbar in „Regeln".
- Versionierung: In „Regeln" Versionsinfo führen (Datum, Autor, Änderung).
Lieferumfang (empfohlen):
- Vorlage (.xltx) + Beispielmappe mit ausgefülltem Muster-Monat,
- CSV-Schablone für Feiertage (Spalten: Datum;Name;BL),
- Screenshot/Notiz der Datenvalidierungen und bedingten Formatierungen.
## Mini-Changelog
- 14.11.2025: Umstellung auf Variante 2 (streng). WE-Vergütung nur bei WE_Summe ≥ 2,0,
anschließend Abzug 1,0 (Freitag zuerst). Unterhalb der Schwelle: WE-Auszahlung = 0 €.
- 13.11.2025: Vorversion (Variante 1) mit WE-Auszahlung ab erstem WE-Dienst und Abzug nach Schwelle (ersetzt).
## Kurztext (für Blatt „Regeln" als Readme-Hinweis)
„WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig). Variante 2 (streng): WE werden nur vergütet, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden; dann 450 €/WE und Abzug 1,0 (Freitag zuerst). WT werden immer mit 250 € vergütet. Splits anteilig. Monat und Bundesland oben wählen."
— Ende der README —

1
output/.gitkeep Normal file
View file

@ -0,0 +1 @@
# Ausgabeverzeichnis für Excel-Dateien

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
openpyxl==3.1.2

301
src/build_template.py Normal file
View file

@ -0,0 +1,301 @@
"""Builds an empty Excel template for the NRW duty roster rules (Variante 2 - streng)."""
from pathlib import Path
from datetime import date
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.worksheet.table import Table, TableStyleInfo
from openpyxl.utils import get_column_letter
TEMPLATE_PATH = Path("templates/Dienstplan_Vorlage_V2_NRW.xlsx")
MAX_PLAN_ROWS = 400
MAX_HOLIDAY_ROWS = 50
NRW_HOLIDAYS_2025 = [
("2025-01-01", "Neujahr", "NRW"),
("2025-04-18", "Karfreitag", "NRW"),
("2025-04-21", "Ostermontag", "NRW"),
("2025-05-01", "Tag der Arbeit", "NRW"),
("2025-05-29", "Christi Himmelfahrt", "NRW"),
("2025-06-09", "Pfingstmontag", "NRW"),
("2025-06-19", "Fronleichnam", "NRW"),
("2025-10-03", "Tag der Deutschen Einheit", "NRW"),
("2025-11-01", "Allerheiligen", "NRW"),
("2025-12-25", "1. Weihnachtstag", "NRW"),
("2025-12-26", "2. Weihnachtstag", "NRW"),
]
NRW_HOLIDAYS_2026 = [
("2026-01-01", "Neujahr", "NRW"),
("2026-04-03", "Karfreitag", "NRW"),
("2026-04-06", "Ostermontag", "NRW"),
("2026-05-01", "Tag der Arbeit", "NRW"),
("2026-05-14", "Christi Himmelfahrt", "NRW"),
("2026-05-25", "Pfingstmontag", "NRW"),
("2026-06-04", "Fronleichnam", "NRW"),
("2026-10-03", "Tag der Deutschen Einheit", "NRW"),
("2026-11-01", "Allerheiligen", "NRW"),
("2026-12-25", "1. Weihnachtstag", "NRW"),
("2026-12-26", "2. Weihnachtstag", "NRW"),
]
def _style_header(ws, row=1):
"""Apply header styling."""
fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
font = Font(bold=True, color="FFFFFF", size=11)
for cell in ws[row]:
if cell.value:
cell.font = font
cell.fill = fill
cell.alignment = Alignment(horizontal="center", vertical="center")
def _populate_readme(ws):
ws["A1"] = "NRW-Dienstplan (Variante 2 streng)"
ws["A1"].font = Font(bold=True, size=14)
ws["A3"] = "Kurzregeln"
ws["A3"].font = Font(bold=True)
rules = [
"WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig).",
"Variante 2 (streng): WE werden nur vergütet, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden;",
"dann 450 €/WE und Abzug 1,0 (Freitag zuerst). WT werden immer mit 250 € vergütet.",
"Splits anteilig. Monat und Bundesland in 'Regeln' wählen.",
"",
"Schritte:",
"1. In 'Regeln': Monat_Auswahl (erster Tag) + BL_Auswahl setzen.",
"2. 'Feiertage' kontrollieren bzw. erweitern.",
"3. Im Blatt 'Plan' pro Tag Datum, Mitarbeiter und Anteil (01) eintragen.",
"4. Auswertung erfolgt automatisch im Blatt 'Auswertung'.",
"5. 'Checks' zeigt Unstimmigkeiten (Summe Anteil ≠ 1, etc.).",
]
for idx, text in enumerate(rules, start=4):
ws[f"A{idx}"] = text
ws.column_dimensions["A"].width = 100
def _populate_rules(ws):
headers = ["Parameter", "Wert", "Beschreibung"]
ws.append(headers)
rows = [
("Satz_WT", 250, "Euro für jeden Werktagsdienst (MoDo, sofern kein WE-Tag)"),
("Satz_WE", 450, "Euro für jeden WE-Tag (FrSo, Feiertag, Vortag Feiertag)"),
("WE_Schwelle", 2.0, "Ab dieser WE-Anzahl wird vergütet (sonst 0 €)"),
("Abzug_nach_WE_Schwelle", 1.0, "Einheiten, die nach Erreichen der Schwelle abgezogen werden"),
("BL_Auswahl", "NRW", "Bundesland (steuert Feiertage)"),
("Monat_Auswahl", date(2025, 11, 1), "Erster Tag des Zielmonats"),
("Variante", 2, "Fix: 2 = streng (WE nur bei Schwelle ≥ 2,0)"),
]
for param, value, desc in rows:
ws.append([param, value, desc])
ws.column_dimensions["A"].width = 26
ws.column_dimensions["B"].width = 18
ws.column_dimensions["C"].width = 80
_style_header(ws)
def _populate_holidays(ws):
headers = ["Datum", "Name", "BL"]
ws.append(headers)
all_holidays = NRW_HOLIDAYS_2025 + NRW_HOLIDAYS_2026
for iso_date, name, bl in all_holidays:
ws.append([iso_date, name, bl])
# Create table
tab = Table(displayName="tblFeiertage", ref=f"A1:C{len(all_holidays)+1}")
style = TableStyleInfo(name="TableStyleMedium9", showFirstColumn=False,
showLastColumn=False, showRowStripes=True, showColumnStripes=False)
tab.tableStyleInfo = style
ws.add_table(tab)
ws.column_dimensions["A"].width = 14
ws.column_dimensions["B"].width = 32
ws.column_dimensions["C"].width = 8
_style_header(ws)
def _plan_formulas(row: int) -> dict:
"""Return helper-column formulas for Plan sheet (Variante 2)."""
date_cell = f"A{row}"
anteil_cell = f"C{row}"
# Holiday range filtered by BL (Non-365 fallback with SUMPRODUCT)
holiday_check = f'SUMMENPRODUKT((tblFeiertage[Datum]={date_cell})*(tblFeiertage[BL]=Regeln!$B$6))>0'
vortag_check = f'SUMMENPRODUKT((tblFeiertage[Datum]={date_cell}+1)*(tblFeiertage[BL]=Regeln!$B$6))>0'
return {
"D": f"=WENNFEHLER({holiday_check};FALSCH)", # Ist_FEIERTAG
"E": f"=WENNFEHLER({vortag_check};FALSCH)", # Ist_VORTAG
"F": f"=WENNFEHLER(WOCHENTAG({date_cell};2)=5;FALSCH)", # Ist_Freitag
"G": f"=ODER($F{row};WOCHENTAG({date_cell};2)=6;WOCHENTAG({date_cell};2)=7;$D{row};$E{row})", # Ist_WE_Tag
"H": f"=NICHT($G{row})", # Ist_WT_Tag
"I": f"=WENN($H{row};{anteil_cell};0)", # WT_Einheit
"J": f"=WENN(UND($G{row};$F{row});{anteil_cell};0)", # WE_Freitag_Einheit
"K": f"=WENN(UND($G{row};NICHT($F{row}));{anteil_cell};0)", # WE_Andere_Einheit
}
def _populate_plan(ws):
headers = [
"Datum", "Mitarbeiter", "Anteil",
"Ist_FEIERTAG", "Ist_VORTAG", "Ist_Freitag", "Ist_WE_Tag", "Ist_WT_Tag",
"WT_Einheit", "WE_Freitag_Einheit", "WE_Andere_Einheit"
]
ws.append(headers)
_style_header(ws)
for row in range(2, MAX_PLAN_ROWS + 2):
formulas = _plan_formulas(row)
for col_letter, formula in formulas.items():
ws[f"{col_letter}{row}"] = formula
# Table
tab = Table(displayName="tblPlan", ref=f"A1:K{MAX_PLAN_ROWS+1}")
style = TableStyleInfo(name="TableStyleMedium2", showFirstColumn=False,
showLastColumn=False, showRowStripes=True, showColumnStripes=False)
tab.tableStyleInfo = style
ws.add_table(tab)
# Data validation for Anteil
dv = DataValidation(type="decimal", operator="between", formula1="0", formula2="1", allow_blank=True)
ws.add_data_validation(dv)
dv.add(f"C2:C{MAX_PLAN_ROWS + 1}")
ws.column_dimensions["A"].width = 12
ws.column_dimensions["B"].width = 22
ws.column_dimensions["C"].width = 10
for col in "DEFGHIJK":
ws.column_dimensions[col].width = 13
def _populate_auswertung(ws):
headers = [
"Mitarbeiter", "WT_Einheiten", "WE_Freitag", "WE_Andere", "WE_Gesamt",
"Schwelle_erreicht", "Abzug_gesamt", "Abzug_Freitag", "Abzug_Andere",
"WE_bezahlt", "Auszahlung_WT", "Auszahlung_WE", "Auszahlung_Gesamt"
]
ws.append(headers)
_style_header(ws)
# Manual employee list (user fills column A)
# Row 2 onwards: formulas reference column A
monat_start = "Regeln!$B$7"
monat_end = f"MONATSENDE({monat_start};0)"
# Create formulas for 50 rows
for row in range(2, 52):
name_ref = f"$A{row}"
# Skip if no name
guard = f'WENN({name_ref}="";""'
# WT_Einheiten - using SUMMENPRODUKT for compatibility
wt_formula = (
f'={guard};SUMMENPRODUKT((tblPlan[Mitarbeiter]={name_ref})*'
f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*'
f'(tblPlan[WT_Einheit])))'
)
ws[f"B{row}"] = wt_formula
# WE_Freitag
we_fri_formula = (
f'={guard};SUMMENPRODUKT((tblPlan[Mitarbeiter]={name_ref})*'
f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*'
f'(tblPlan[WE_Freitag_Einheit])))'
)
ws[f"C{row}"] = we_fri_formula
# WE_Andere
we_other_formula = (
f'={guard};SUMMENPRODUKT((tblPlan[Mitarbeiter]={name_ref})*'
f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*'
f'(tblPlan[WE_Andere_Einheit])))'
)
ws[f"D{row}"] = we_other_formula
# WE_Gesamt
ws[f"E{row}"] = f'={guard};C{row}+D{row})'
# Schwelle_erreicht
ws[f"F{row}"] = f'={guard};WENN(E{row}>=Regeln!$B$4-0,0001;"JA";"NEIN"))'
# Abzug_gesamt
ws[f"G{row}"] = f'={guard};WENN(E{row}>=Regeln!$B$4-0,0001;Regeln!$B$5;0))'
# Abzug_Freitag
ws[f"H{row}"] = f'={guard};MIN(G{row};C{row}))'
# Abzug_Andere
ws[f"I{row}"] = f'={guard};MAX(0;G{row}-H{row}))'
# WE_bezahlt (Variante 2: only if threshold reached)
ws[f"J{row}"] = f'={guard};WENN(E{row}<Regeln!$B$4-0,0001;0;(C{row}-H{row})+(D{row}-I{row})))'
# Auszahlung_WT
ws[f"K{row}"] = f'={guard};B{row}*Regeln!$B$2)'
# Auszahlung_WE
ws[f"L{row}"] = f'={guard};J{row}*Regeln!$B$3)'
# Auszahlung_Gesamt
ws[f"M{row}"] = f'={guard};K{row}+L{row})'
widths = [22, 14, 14, 14, 14, 16, 14, 14, 14, 14, 14, 14, 16]
for idx, width in enumerate(widths, start=1):
ws.column_dimensions[get_column_letter(idx)].width = width
def _populate_checks(ws):
ws["A1"] = "Datum"
ws["B1"] = "Summe_Anteile"
ws["C1"] = "Status"
_style_header(ws)
# Manual check list - user can add dates to check
# Formula checks sum of Anteil for each date
for row in range(2, 52):
date_ref = f"A{row}"
ws[f"B{row}"] = f'=WENN({date_ref}="";"";SUMMENPRODUKT((tblPlan[Datum]={date_ref})*(tblPlan[Anteil])))'
ws[f"C{row}"] = f'=WENN({date_ref}="";""WENN(ABS(B{row}-1)<=0,0001;"OK";"FEHLER"))'
ws.column_dimensions["A"].width = 14
ws.column_dimensions["B"].width = 16
ws.column_dimensions["C"].width = 12
def build_template():
TEMPLATE_PATH.parent.mkdir(parents=True, exist_ok=True)
wb = Workbook()
readme_ws = wb.active
readme_ws.title = "README"
_populate_readme(readme_ws)
rules_ws = wb.create_sheet("Regeln")
_populate_rules(rules_ws)
holiday_ws = wb.create_sheet("Feiertage")
_populate_holidays(holiday_ws)
plan_ws = wb.create_sheet("Plan")
_populate_plan(plan_ws)
auswertung_ws = wb.create_sheet("Auswertung")
_populate_auswertung(auswertung_ws)
checks_ws = wb.create_sheet("Checks")
_populate_checks(checks_ws)
wb.save(TEMPLATE_PATH)
return TEMPLATE_PATH
if __name__ == "__main__":
path = build_template()
print(f"✅ Vorlage (Variante 2 streng) erstellt: {path}")

76
src/fill_plan_dates.py Normal file
View file

@ -0,0 +1,76 @@
"""
Füllt das Plan-Blatt automatisch mit allen Datumszeilen eines Monats vor.
Nutzer muss nur noch Namen + Anteile eintragen.
"""
from pathlib import Path
from datetime import date, timedelta
from openpyxl import load_workbook
import sys
def fill_plan_with_dates(template_path, output_path, year, month):
"""
Lädt die Vorlage und füllt Spalte A (Datum) im Plan-Blatt
mit allen Tagen des angegebenen Monats.
"""
wb = load_workbook(template_path)
# Regeln-Blatt: Monat_Auswahl setzen
if "Regeln" in wb.sheetnames:
regeln_ws = wb["Regeln"]
# Zeile 7, Spalte B = Monat_Auswahl
regeln_ws["B7"] = date(year, month, 1)
# Plan-Blatt füllen
if "Plan" not in wb.sheetnames:
print("❌ Blatt 'Plan' nicht gefunden!")
return
plan_ws = wb["Plan"]
# Startdatum
start_date = date(year, month, 1)
# Letzter Tag des Monats
if month == 12:
end_date = date(year + 1, 1, 1) - timedelta(days=1)
else:
end_date = date(year, month + 1, 1) - timedelta(days=1)
# Alle Tage durchgehen
current_date = start_date
row = 2 # Zeile 2 = erste Datenzeile nach Header
while current_date <= end_date:
plan_ws[f"A{row}"] = current_date
# Spalten B (Mitarbeiter) und C (Anteil) bleiben leer zum Ausfüllen
current_date += timedelta(days=1)
row += 1
wb.save(output_path)
print(f"✅ Plan-Blatt vorbefüllt für {month:02d}/{year}")
print(f" Ausgabe: {output_path}")
print(f" Trage jetzt nur noch in Spalte B (Mitarbeiter) und C (Anteil) die Namen ein!")
if __name__ == "__main__":
template = Path("templates/Dienstplan_Vorlage_V2_NRW.xlsx")
if len(sys.argv) >= 3:
year = int(sys.argv[1])
month = int(sys.argv[2])
else:
# Standard: November 2025
year = 2025
month = 11
output = Path(f"output/Dienstplan_{year}_{month:02d}_NRW.xlsx")
output.parent.mkdir(exist_ok=True)
if not template.exists():
print(f"❌ Vorlage nicht gefunden: {template}")
print(" Führe erst 'python src/build_template.py' aus!")
sys.exit(1)
fill_plan_with_dates(template, output, year, month)

63
src/main.py Normal file
View file

@ -0,0 +1,63 @@
"""
Excel XLSX Generator
Erstellt Excel-Dateien mit openpyxl
"""
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
from pathlib import Path
from datetime import datetime
def create_example_excel():
"""Erstellt eine Beispiel-Excel-Datei mit formatierten Daten."""
# Neues Workbook erstellen
wb = Workbook()
ws = wb.active
ws.title = "Beispiel"
# Überschriften hinzufügen
headers = ["Name", "Alter", "Stadt", "Beruf"]
ws.append(headers)
# Überschriften formatieren
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=12)
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center")
# Beispieldaten hinzufügen
data = [
["Max Mustermann", 30, "Berlin", "Entwickler"],
["Erika Musterfrau", 28, "München", "Designerin"],
["Hans Schmidt", 35, "Hamburg", "Manager"],
["Anna Weber", 27, "Köln", "Analyst"],
]
for row in data:
ws.append(row)
# Spaltenbreiten anpassen
ws.column_dimensions['A'].width = 20
ws.column_dimensions['B'].width = 10
ws.column_dimensions['C'].width = 15
ws.column_dimensions['D'].width = 15
# Ausgabeverzeichnis erstellen
output_dir = Path("output")
output_dir.mkdir(exist_ok=True)
# Datei speichern
output_file = output_dir / f"example_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
wb.save(output_file)
print(f"Excel-Datei erfolgreich erstellt: {output_file}")
return output_file
if __name__ == "__main__":
create_example_excel()

81
src/read_excel.py Normal file
View file

@ -0,0 +1,81 @@
"""
Excel-Datei einlesen und Inhalt anzeigen
"""
from openpyxl import load_workbook
import json
from pathlib import Path
def read_excel_to_dict(filepath):
"""Liest eine Excel-Datei und gibt die Daten als Dictionary zurück."""
wb = load_workbook(filepath, data_only=True)
result = {}
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
# Daten aus dem Sheet lesen
data = []
for row in ws.iter_rows(values_only=True):
# Nur Zeilen mit Inhalt
if any(cell is not None for cell in row):
data.append(list(row))
result[sheet_name] = data
return result
def print_excel_content(filepath):
"""Gibt den Inhalt einer Excel-Datei formatiert aus."""
print(f"\n{'='*60}")
print(f"Excel-Datei: {filepath}")
print(f"{'='*60}\n")
data = read_excel_to_dict(filepath)
for sheet_name, rows in data.items():
print(f"\n📊 Sheet: {sheet_name}")
print(f"{'-'*60}")
if not rows:
print(" (leer)")
continue
# Tabelle ausgeben
for i, row in enumerate(rows, 1):
row_str = " | ".join(str(cell) if cell is not None else "" for cell in row)
print(f" {i:3d}: {row_str}")
print(f"\n{'='*60}\n")
# Als JSON ausgeben
print("📄 JSON-Format:")
print(json.dumps(data, indent=2, ensure_ascii=False))
return data
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
# Datei als Argument übergeben
filepath = sys.argv[1]
else:
# Nach neuester Datei im output-Ordner suchen
output_dir = Path("output")
excel_files = list(output_dir.glob("*.xlsx"))
if not excel_files:
print("❌ Keine Excel-Dateien im output-Ordner gefunden!")
print("Verwendung: python src/read_excel.py <pfad-zur-datei>")
sys.exit(1)
# Neueste Datei verwenden
filepath = max(excel_files, key=lambda p: p.stat().st_mtime)
print_excel_content(filepath)