From 99480bb7ff4df98e44103dc6b8d61b76999274f9 Mon Sep 17 00:00:00 2001 From: Kenearos Date: Fri, 14 Nov 2025 14:43:34 +0100 Subject: [PATCH] Initial commit: NRW Dienstplan Generator (Variante 2) --- .github/copilot-instructions.md | 21 ++ .gitignore | 23 +++ README.md | 65 +++++++ SPECIFICATION.md | 328 ++++++++++++++++++++++++++++++++ output/.gitkeep | 1 + requirements.txt | 1 + src/build_template.py | 301 +++++++++++++++++++++++++++++ src/fill_plan_dates.py | 76 ++++++++ src/main.py | 63 ++++++ src/read_excel.py | 81 ++++++++ 10 files changed, 960 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 SPECIFICATION.md create mode 100644 output/.gitkeep create mode 100644 requirements.txt create mode 100644 src/build_template.py create mode 100644 src/fill_plan_dates.py create mode 100644 src/main.py create mode 100644 src/read_excel.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..71cb68a --- /dev/null +++ b/.github/copilot-instructions.md @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbe1afd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..59f9cb7 --- /dev/null +++ b/README.md @@ -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. diff --git a/SPECIFICATION.md b/SPECIFICATION.md new file mode 100644 index 0000000..3550883 --- /dev/null +++ b/SPECIFICATION.md @@ -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="&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;"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 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 — diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..9f6f37d --- /dev/null +++ b/output/.gitkeep @@ -0,0 +1 @@ +# Ausgabeverzeichnis für Excel-Dateien diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1e053ce --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +openpyxl==3.1.2 diff --git a/src/build_template.py b/src/build_template.py new file mode 100644 index 0000000..e7c28db --- /dev/null +++ b/src/build_template.py @@ -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}= 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) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..0d23dde --- /dev/null +++ b/src/main.py @@ -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() diff --git a/src/read_excel.py b/src/read_excel.py new file mode 100644 index 0000000..c5e09ef --- /dev/null +++ b/src/read_excel.py @@ -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 ") + sys.exit(1) + + # Neueste Datei verwenden + filepath = max(excel_files, key=lambda p: p.stat().st_mtime) + + print_excel_content(filepath)