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