Initial commit: NRW Dienstplan Generator (Variante 2)
This commit is contained in:
commit
99480bb7ff
10 changed files with 960 additions and 0 deletions
21
.github/copilot-instructions.md
vendored
Normal file
21
.github/copilot-instructions.md
vendored
Normal 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
23
.gitignore
vendored
Normal 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
65
README.md
Normal 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 Fr–So, 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
328
SPECIFICATION.md
Normal 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<Anteil≤1; 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 Nicht‑365 stehen SUMMENPRODUKT‑Alternativen 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) Nicht‑365‑Alternativen (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<Anteil≤1).
|
||||||
|
- 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
1
output/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Ausgabeverzeichnis für Excel-Dateien
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
openpyxl==3.1.2
|
||||||
301
src/build_template.py
Normal file
301
src/build_template.py
Normal 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 (0–1) 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 (Mo–Do, sofern kein WE-Tag)"),
|
||||||
|
("Satz_WE", 450, "Euro für jeden WE-Tag (Fr–So, 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
76
src/fill_plan_dates.py
Normal 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
63
src/main.py
Normal 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
81
src/read_excel.py
Normal 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)
|
||||||
Reference in a new issue