Vereinfachte Dienstplan-Version: Nur Datum & Mitarbeiter, Python-Berechnungen

- Neue vereinfachte Vorlage ohne komplexe Excel-Formeln
- Automatische Anteil-Berechnung (1 MA = 1.0, 2 MA = je 0.5)
- Python-basierte Vergütungsberechnung nach NRW-Regeln
- Datumsformat als Text für bessere Kompatibilität
- Testdaten-Generator mit Splits
This commit is contained in:
Kenearos 2025-11-18 21:37:25 +00:00
parent 35de2c27f0
commit 034b398c2c
6 changed files with 571 additions and 30 deletions

69
src/add_test_data.py Normal file
View file

@ -0,0 +1,69 @@
"""
Fügt Testdaten in die Dienstplan-Datei ein
"""
from pathlib import Path
from openpyxl import load_workbook
import sys
def add_test_data(filepath):
"""Fügt Testdaten für November 2025 ein."""
wb = load_workbook(filepath)
if "Plan" not in wb.sheetnames:
print("❌ Blatt 'Plan' nicht gefunden!")
return
plan_ws = wb["Plan"]
# Testdaten: Mitarbeiter Namen und Anteile für die ersten Tage
test_entries = [
("Max Mustermann", 1.0),
("Anna Schmidt", 1.0),
("Max Mustermann", 1.0),
("Peter Klein", 0.5),
("Anna Schmidt", 0.5),
("Max Mustermann", 1.0),
("Anna Schmidt", 1.0),
("Peter Klein", 1.0),
("Max Mustermann", 0.5),
("Anna Schmidt", 0.5),
("Max Mustermann", 1.0),
("Peter Klein", 1.0),
("Anna Schmidt", 1.0),
("Max Mustermann", 1.0),
("Peter Klein", 0.5),
("Anna Schmidt", 0.5),
]
# Füge die Daten ab Zeile 2 ein
for i, (name, anteil) in enumerate(test_entries, start=2):
plan_ws[f"B{i}"] = name
plan_ws[f"C{i}"] = anteil
# Mitarbeiterliste in Auswertung
if "Auswertung" in wb.sheetnames:
auswertung_ws = wb["Auswertung"]
mitarbeiter = ["Max Mustermann", "Anna Schmidt", "Peter Klein"]
for i, name in enumerate(mitarbeiter, start=2):
auswertung_ws[f"A{i}"] = name
wb.save(filepath)
print(f"✅ Testdaten eingefügt in {filepath}")
print(f" Mitarbeiter: Max Mustermann, Anna Schmidt, Peter Klein")
print(f" {len(test_entries)} Einträge hinzugefügt")
if __name__ == "__main__":
if len(sys.argv) >= 2:
filepath = Path(sys.argv[1])
else:
filepath = Path("output/Dienstplan_2025_11_NRW.xlsx")
if not filepath.exists():
print(f"❌ Datei nicht gefunden: {filepath}")
sys.exit(1)
add_test_data(filepath)

View file

@ -4,7 +4,7 @@ from pathlib import Path
from datetime import date from datetime import date
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.styles import Alignment, Font, PatternFill, Border, Side, numbers
from openpyxl.worksheet.datavalidation import DataValidation from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.worksheet.table import Table, TableStyleInfo from openpyxl.worksheet.table import Table, TableStyleInfo
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
@ -103,7 +103,10 @@ def _populate_holidays(ws):
all_holidays = NRW_HOLIDAYS_2025 + NRW_HOLIDAYS_2026 all_holidays = NRW_HOLIDAYS_2025 + NRW_HOLIDAYS_2026
for iso_date, name, bl in all_holidays: for iso_date, name, bl in all_holidays:
ws.append([iso_date, name, bl]) # Convert ISO string to date object
year, month, day = iso_date.split('-')
date_obj = date(int(year), int(month), int(day))
ws.append([date_obj, name, bl])
# Create table # Create table
tab = Table(displayName="tblFeiertage", ref=f"A1:C{len(all_holidays)+1}") tab = Table(displayName="tblFeiertage", ref=f"A1:C{len(all_holidays)+1}")
@ -117,6 +120,10 @@ def _populate_holidays(ws):
ws.column_dimensions["C"].width = 8 ws.column_dimensions["C"].width = 8
_style_header(ws) _style_header(ws)
# Format column A as date
for row in range(2, len(all_holidays) + 2):
ws[f"A{row}"].number_format = 'DD.MM.YYYY'
def _plan_formulas(row: int) -> dict: def _plan_formulas(row: int) -> dict:
"""Return helper-column formulas for Plan sheet (Variante 2).""" """Return helper-column formulas for Plan sheet (Variante 2)."""
@ -124,18 +131,18 @@ def _plan_formulas(row: int) -> dict:
anteil_cell = f"C{row}" anteil_cell = f"C{row}"
# Holiday range filtered by BL (Non-365 fallback with SUMPRODUCT) # Holiday range filtered by BL (Non-365 fallback with SUMPRODUCT)
holiday_check = f'SUMMENPRODUKT((tblFeiertage[Datum]={date_cell})*(tblFeiertage[BL]=Regeln!$B$6))>0' holiday_check = f'SUMPRODUCT((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' vortag_check = f'SUMPRODUCT((tblFeiertage[Datum]={date_cell}+1)*(tblFeiertage[BL]=Regeln!$B$6))>0'
return { return {
"D": f"=WENNFEHLER({holiday_check};FALSCH)", # Ist_FEIERTAG "D": f"=IFERROR({holiday_check},FALSE)", # Ist_FEIERTAG
"E": f"=WENNFEHLER({vortag_check};FALSCH)", # Ist_VORTAG "E": f"=IFERROR({vortag_check},FALSE)", # Ist_VORTAG
"F": f"=WENNFEHLER(WOCHENTAG({date_cell};2)=5;FALSCH)", # Ist_Freitag "F": f"=IFERROR(WEEKDAY({date_cell},2)=5,FALSE)", # Ist_Freitag
"G": f"=ODER($F{row};WOCHENTAG({date_cell};2)=6;WOCHENTAG({date_cell};2)=7;$D{row};$E{row})", # Ist_WE_Tag "G": f"=OR($F{row},WEEKDAY({date_cell},2)=6,WEEKDAY({date_cell},2)=7,$D{row},$E{row})", # Ist_WE_Tag
"H": f"=NICHT($G{row})", # Ist_WT_Tag "H": f"=NOT($G{row})", # Ist_WT_Tag
"I": f"=WENN($H{row};{anteil_cell};0)", # WT_Einheit "I": f"=IF($H{row},{anteil_cell},0)", # WT_Einheit
"J": f"=WENN(UND($G{row};$F{row});{anteil_cell};0)", # WE_Freitag_Einheit "J": f"=IF(AND($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 "K": f"=IF(AND($G{row},NOT($F{row})),{anteil_cell},0)", # WE_Andere_Einheit
} }
@ -171,6 +178,10 @@ def _populate_plan(ws):
for col in "DEFGHIJK": for col in "DEFGHIJK":
ws.column_dimensions[col].width = 13 ws.column_dimensions[col].width = 13
# Format column A as date
for row in range(2, MAX_PLAN_ROWS + 2):
ws[f"A{row}"].number_format = 'DD.MM.YYYY'
def _populate_auswertung(ws): def _populate_auswertung(ws):
headers = [ headers = [
@ -185,18 +196,18 @@ def _populate_auswertung(ws):
# Row 2 onwards: formulas reference column A # Row 2 onwards: formulas reference column A
monat_start = "Regeln!$B$7" monat_start = "Regeln!$B$7"
monat_end = f"MONATSENDE({monat_start};0)" monat_end = f"EOMONTH({monat_start},0)"
# Create formulas for 50 rows # Create formulas for 50 rows
for row in range(2, 52): for row in range(2, 52):
name_ref = f"$A{row}" name_ref = f"$A{row}"
# Skip if no name # Skip if no name
guard = f'WENN({name_ref}="";""' guard = f'IF({name_ref}="",""'
# WT_Einheiten - using SUMMENPRODUKT for compatibility # WT_Einheiten - using SUMPRODUCT for compatibility
wt_formula = ( wt_formula = (
f'={guard};SUMMENPRODUKT((tblPlan[Mitarbeiter]={name_ref})*' f'={guard},SUMPRODUCT((tblPlan[Mitarbeiter]={name_ref})*'
f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*' f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*'
f'(tblPlan[WT_Einheit])))' f'(tblPlan[WT_Einheit])))'
) )
@ -204,7 +215,7 @@ def _populate_auswertung(ws):
# WE_Freitag # WE_Freitag
we_fri_formula = ( we_fri_formula = (
f'={guard};SUMMENPRODUKT((tblPlan[Mitarbeiter]={name_ref})*' f'={guard},SUMPRODUCT((tblPlan[Mitarbeiter]={name_ref})*'
f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*' f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*'
f'(tblPlan[WE_Freitag_Einheit])))' f'(tblPlan[WE_Freitag_Einheit])))'
) )
@ -212,38 +223,38 @@ def _populate_auswertung(ws):
# WE_Andere # WE_Andere
we_other_formula = ( we_other_formula = (
f'={guard};SUMMENPRODUKT((tblPlan[Mitarbeiter]={name_ref})*' f'={guard},SUMPRODUCT((tblPlan[Mitarbeiter]={name_ref})*'
f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*' f'(tblPlan[Datum]>={monat_start})*(tblPlan[Datum]<={monat_end})*'
f'(tblPlan[WE_Andere_Einheit])))' f'(tblPlan[WE_Andere_Einheit])))'
) )
ws[f"D{row}"] = we_other_formula ws[f"D{row}"] = we_other_formula
# WE_Gesamt # WE_Gesamt
ws[f"E{row}"] = f'={guard};C{row}+D{row})' ws[f"E{row}"] = f'={guard},C{row}+D{row})'
# Schwelle_erreicht # Schwelle_erreicht
ws[f"F{row}"] = f'={guard};WENN(E{row}>=Regeln!$B$4-0,0001;"JA";"NEIN"))' ws[f"F{row}"] = f'={guard},IF(E{row}>=Regeln!$B$4-0.0001,"JA","NEIN"))'
# Abzug_gesamt # Abzug_gesamt
ws[f"G{row}"] = f'={guard};WENN(E{row}>=Regeln!$B$4-0,0001;Regeln!$B$5;0))' ws[f"G{row}"] = f'={guard},IF(E{row}>=Regeln!$B$4-0.0001,Regeln!$B$5,0))'
# Abzug_Freitag # Abzug_Freitag
ws[f"H{row}"] = f'={guard};MIN(G{row};C{row}))' ws[f"H{row}"] = f'={guard},MIN(G{row},C{row}))'
# Abzug_Andere # Abzug_Andere
ws[f"I{row}"] = f'={guard};MAX(0;G{row}-H{row}))' ws[f"I{row}"] = f'={guard},MAX(0,G{row}-H{row}))'
# WE_bezahlt (Variante 2: only if threshold reached) # 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})))' ws[f"J{row}"] = f'={guard},IF(E{row}<Regeln!$B$4-0.0001,0,(C{row}-H{row})+(D{row}-I{row})))'
# Auszahlung_WT # Auszahlung_WT
ws[f"K{row}"] = f'={guard};B{row}*Regeln!$B$2)' ws[f"K{row}"] = f'={guard},B{row}*Regeln!$B$2)'
# Auszahlung_WE # Auszahlung_WE
ws[f"L{row}"] = f'={guard};J{row}*Regeln!$B$3)' ws[f"L{row}"] = f'={guard},J{row}*Regeln!$B$3)'
# Auszahlung_Gesamt # Auszahlung_Gesamt
ws[f"M{row}"] = f'={guard};K{row}+L{row})' ws[f"M{row}"] = f'={guard},K{row}+L{row})'
widths = [22, 14, 14, 14, 14, 16, 14, 14, 14, 14, 14, 14, 16] widths = [22, 14, 14, 14, 14, 16, 14, 14, 14, 14, 14, 14, 16]
for idx, width in enumerate(widths, start=1): for idx, width in enumerate(widths, start=1):
@ -260,8 +271,8 @@ def _populate_checks(ws):
# Formula checks sum of Anteil for each date # Formula checks sum of Anteil for each date
for row in range(2, 52): for row in range(2, 52):
date_ref = f"A{row}" date_ref = f"A{row}"
ws[f"B{row}"] = f'=WENN({date_ref}="";"";SUMMENPRODUKT((tblPlan[Datum]={date_ref})*(tblPlan[Anteil])))' ws[f"B{row}"] = f'=IF({date_ref}="","",SUMPRODUCT((tblPlan[Datum]={date_ref})*(tblPlan[Anteil])))'
ws[f"C{row}"] = f'=WENN({date_ref}="";"";WENN(ABS(B{row}-1)<=0,0001;"OK";"FEHLER"))' ws[f"C{row}"] = f'=IF({date_ref}="","",IF(ABS(B{row}-1)<=0.0001,"OK","FEHLER"))'
ws.column_dimensions["A"].width = 14 ws.column_dimensions["A"].width = 14
ws.column_dimensions["B"].width = 16 ws.column_dimensions["B"].width = 16

View file

@ -0,0 +1,120 @@
"""Builds a simplified Excel template without complex formulas."""
from pathlib import Path
from datetime import date, datetime
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill, numbers
from openpyxl.worksheet.datavalidation import DataValidation
TEMPLATE_PATH = Path("templates/Dienstplan_Vorlage_V2_NRW_Simple.xlsx")
NRW_HOLIDAYS_2025 = [
(date(2025, 1, 1), "Neujahr", "NRW"),
(date(2025, 4, 18), "Karfreitag", "NRW"),
(date(2025, 4, 21), "Ostermontag", "NRW"),
(date(2025, 5, 1), "Tag der Arbeit", "NRW"),
(date(2025, 5, 29), "Christi Himmelfahrt", "NRW"),
(date(2025, 6, 9), "Pfingstmontag", "NRW"),
(date(2025, 6, 19), "Fronleichnam", "NRW"),
(date(2025, 10, 3), "Tag der Deutschen Einheit", "NRW"),
(date(2025, 11, 1), "Allerheiligen", "NRW"),
(date(2025, 12, 25), "1. Weihnachtstag", "NRW"),
(date(2025, 12, 26), "2. Weihnachtstag", "NRW"),
]
NRW_HOLIDAYS_2026 = [
(date(2026, 1, 1), "Neujahr", "NRW"),
(date(2026, 4, 3), "Karfreitag", "NRW"),
(date(2026, 4, 6), "Ostermontag", "NRW"),
(date(2026, 5, 1), "Tag der Arbeit", "NRW"),
(date(2026, 5, 14), "Christi Himmelfahrt", "NRW"),
(date(2026, 5, 25), "Pfingstmontag", "NRW"),
(date(2026, 6, 4), "Fronleichnam", "NRW"),
(date(2026, 10, 3), "Tag der Deutschen Einheit", "NRW"),
(date(2026, 11, 1), "Allerheiligen", "NRW"),
(date(2026, 12, 25), "1. Weihnachtstag", "NRW"),
(date(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 build_simple_template():
"""Creates a simple template without complex formulas."""
TEMPLATE_PATH.parent.mkdir(parents=True, exist_ok=True)
wb = Workbook()
# README
readme_ws = wb.active
readme_ws.title = "README"
readme_ws["A1"] = "NRW-Dienstplan - Einfache Version"
readme_ws["A1"].font = Font(bold=True, size=14)
readme_ws["A3"] = "Anleitung:"
readme_ws["A4"] = "1. Trage im Blatt 'Plan' nur Datum und Mitarbeiter ein"
readme_ws["A5"] = "2. Pro Tag kannst du 1 oder 2 Mitarbeiter eintragen (Split = je 0.5)"
readme_ws["A6"] = "3. Führe 'python src/calculate.py' aus, um die Vergütung zu berechnen"
readme_ws["A7"] = "4. Das Ergebnis erscheint im Blatt 'Auswertung'"
readme_ws.column_dimensions["A"].width = 80
# Feiertage
holiday_ws = wb.create_sheet("Feiertage")
holiday_ws.append(["Datum", "Name", "BL"])
all_holidays = NRW_HOLIDAYS_2025 + NRW_HOLIDAYS_2026
for holiday_date, name, bl in all_holidays:
# Format date as string for display
holiday_ws.append([holiday_date.strftime('%d.%m.%Y'), name, bl])
holiday_ws.column_dimensions["A"].width = 14
holiday_ws.column_dimensions["B"].width = 32
holiday_ws.column_dimensions["C"].width = 8
_style_header(holiday_ws)
# Plan (simple input sheet)
plan_ws = wb.create_sheet("Plan")
plan_ws.append(["Datum", "Mitarbeiter"])
_style_header(plan_ws)
plan_ws.column_dimensions["A"].width = 14
plan_ws.column_dimensions["B"].width = 25
# Auswertung (will be filled by Python script)
auswertung_ws = wb.create_sheet("Auswertung")
headers = [
"Mitarbeiter",
"WT_Dienste",
"WE_Dienste_Freitag",
"WE_Dienste_Andere",
"WE_Gesamt",
"Schwelle_erreicht",
"Abzug_Freitag",
"Abzug_Andere",
"WE_bezahlt",
"Auszahlung_WT",
"Auszahlung_WE",
"Auszahlung_Gesamt"
]
auswertung_ws.append(headers)
_style_header(auswertung_ws)
for col_idx in range(1, len(headers) + 1):
auswertung_ws.column_dimensions[chr(64 + col_idx)].width = 16
wb.save(TEMPLATE_PATH)
return TEMPLATE_PATH
if __name__ == "__main__":
path = build_simple_template()
print(f"✅ Einfache Vorlage erstellt: {path}")
print(" Verwende diese mit Python-Berechnungen statt Excel-Formeln")

249
src/calculate.py Normal file
View file

@ -0,0 +1,249 @@
"""
Berechnet die Vergütung aus der Plan-Datei nach NRW-Regeln (Variante 2)
"""
from pathlib import Path
from datetime import datetime, timedelta, date
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill
import sys
from collections import defaultdict
# Vergütungssätze
SATZ_WT = 250 # Euro für Werktag
SATZ_WE = 450 # Euro für Wochenende
WE_SCHWELLE = 2.0 # Mindestanzahl WE-Dienste für Vergütung
ABZUG = 1.0 # Abzug nach Erreichen der Schwelle
def load_holidays(wb):
"""Lädt Feiertage aus dem Feiertage-Blatt."""
if "Feiertage" not in wb.sheetnames:
return set()
holidays = set()
ws = wb["Feiertage"]
for row in ws.iter_rows(min_row=2, values_only=True):
if row[0] and row[2] == "NRW": # Datum und BL prüfen
date_raw = row[0]
if isinstance(date_raw, str):
try:
parsed_date = datetime.strptime(date_raw, '%d.%m.%Y').date()
holidays.add(parsed_date)
except:
pass
elif isinstance(date_raw, datetime):
holidays.add(date_raw.date())
elif isinstance(date_raw, date):
holidays.add(date_raw)
return holidays
def is_we_tag(datum, holidays):
"""Prüft ob ein Datum ein WE-Tag ist (Fr/Sa/So/Feiertag/Vortag)."""
if isinstance(datum, datetime):
datum = datum.date()
# Freitag (4), Samstag (5), Sonntag (6)
weekday = datum.weekday()
if weekday >= 4: # Fr, Sa, So
return True
# Ist Feiertag?
if datum in holidays:
return True
# Ist Vortag eines Feiertags?
next_day = datum + timedelta(days=1)
if next_day in holidays:
return True
return False
def is_freitag(datum):
"""Prüft ob ein Datum ein Freitag ist."""
if isinstance(datum, datetime):
datum = datum.date()
return datum.weekday() == 4
def calculate_verguetung(plan_data, holidays):
"""Berechnet Vergütung je Mitarbeiter. Anteil wird automatisch berechnet."""
# Gruppiere nach Datum und zähle Mitarbeiter
dienste_pro_tag = defaultdict(list)
for datum, mitarbeiter in plan_data:
if mitarbeiter:
dienste_pro_tag[datum].append(mitarbeiter)
# Sammle Daten je Mitarbeiter
mitarbeiter_data = defaultdict(lambda: {
'wt_einheiten': 0.0,
'we_freitag': 0.0,
'we_andere': 0.0
})
# Berechne Anteile automatisch
for datum, mitarbeiter_liste in dienste_pro_tag.items():
anzahl = len(mitarbeiter_liste)
anteil = 1.0 / anzahl if anzahl > 0 else 0
for mitarbeiter in mitarbeiter_liste:
if is_we_tag(datum, holidays):
if is_freitag(datum):
mitarbeiter_data[mitarbeiter]['we_freitag'] += anteil
else:
mitarbeiter_data[mitarbeiter]['we_andere'] += anteil
else:
mitarbeiter_data[mitarbeiter]['wt_einheiten'] += anteil
# Berechne Vergütung
results = []
for mitarbeiter, data in sorted(mitarbeiter_data.items()):
wt = data['wt_einheiten']
we_fri = data['we_freitag']
we_other = data['we_andere']
we_gesamt = we_fri + we_other
# Schwelle erreicht?
schwelle_erreicht = we_gesamt >= (WE_SCHWELLE - 0.0001)
if schwelle_erreicht:
# Abzug von 1.0 WE-Einheit (Freitag zuerst)
abzug_freitag = min(ABZUG, we_fri)
abzug_andere = max(0, ABZUG - abzug_freitag)
# Bezahlte WE-Einheiten
we_bezahlt = (we_fri - abzug_freitag) + (we_other - abzug_andere)
else:
# Schwelle nicht erreicht - keine WE-Vergütung
abzug_freitag = 0
abzug_andere = 0
we_bezahlt = 0
# Auszahlungen
auszahlung_wt = wt * SATZ_WT
auszahlung_we = we_bezahlt * SATZ_WE
auszahlung_gesamt = auszahlung_wt + auszahlung_we
results.append({
'mitarbeiter': mitarbeiter,
'wt_einheiten': wt,
'we_freitag': we_fri,
'we_andere': we_other,
'we_gesamt': we_gesamt,
'schwelle_erreicht': 'JA' if schwelle_erreicht else 'NEIN',
'abzug_freitag': abzug_freitag,
'abzug_andere': abzug_andere,
'we_bezahlt': we_bezahlt,
'auszahlung_wt': auszahlung_wt,
'auszahlung_we': auszahlung_we,
'auszahlung_gesamt': auszahlung_gesamt
})
return results
def process_file(filepath):
"""Verarbeitet die Excel-Datei und schreibt Auswertung."""
wb = load_workbook(filepath)
# Lade Feiertage
holidays = load_holidays(wb)
print(f"📅 {len(holidays)} Feiertage geladen")
# Lade Plan-Daten
if "Plan" not in wb.sheetnames:
print("❌ Blatt 'Plan' nicht gefunden!")
return
plan_ws = wb["Plan"]
plan_data = []
for row in plan_ws.iter_rows(min_row=2, values_only=True):
if row[0]: # Wenn Datum vorhanden
datum_raw = row[0]
mitarbeiter = row[1] if len(row) > 1 else None
# Parse Datum (kann String oder date sein)
if isinstance(datum_raw, str):
try:
datum = datetime.strptime(datum_raw, '%d.%m.%Y').date()
except:
continue
elif isinstance(datum_raw, datetime):
datum = datum_raw.date()
elif isinstance(datum_raw, date):
datum = datum_raw
else:
continue
if mitarbeiter:
plan_data.append((datum, mitarbeiter))
print(f"📋 {len(plan_data)} Einträge im Plan")
# Berechne Vergütung
results = calculate_verguetung(plan_data, holidays)
# Schreibe Auswertung
if "Auswertung" not in wb.sheetnames:
print("❌ Blatt 'Auswertung' nicht gefunden!")
return
auswertung_ws = wb["Auswertung"]
# Lösche alte Daten (ab Zeile 2)
auswertung_ws.delete_rows(2, auswertung_ws.max_row)
# Schreibe neue Daten
for idx, result in enumerate(results, start=2):
auswertung_ws[f"A{idx}"] = result['mitarbeiter']
auswertung_ws[f"B{idx}"] = round(result['wt_einheiten'], 2)
auswertung_ws[f"C{idx}"] = round(result['we_freitag'], 2)
auswertung_ws[f"D{idx}"] = round(result['we_andere'], 2)
auswertung_ws[f"E{idx}"] = round(result['we_gesamt'], 2)
auswertung_ws[f"F{idx}"] = result['schwelle_erreicht']
auswertung_ws[f"G{idx}"] = round(result['abzug_freitag'], 2)
auswertung_ws[f"H{idx}"] = round(result['abzug_andere'], 2)
auswertung_ws[f"I{idx}"] = round(result['we_bezahlt'], 2)
auswertung_ws[f"J{idx}"] = round(result['auszahlung_wt'], 2)
auswertung_ws[f"K{idx}"] = round(result['auszahlung_we'], 2)
auswertung_ws[f"L{idx}"] = round(result['auszahlung_gesamt'], 2)
# Formatierung für Schwelle
if result['schwelle_erreicht'] == 'JA':
auswertung_ws[f"F{idx}"].fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
else:
auswertung_ws[f"F{idx}"].fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
wb.save(filepath)
print(f"\n✅ Auswertung geschrieben: {len(results)} Mitarbeiter")
print(f" Datei: {filepath}")
# Zeige Zusammenfassung
print(f"\n{'='*70}")
print(f"{'Mitarbeiter':<20} {'WT':<8} {'WE':<8} {'Schwelle':<10} {'Gesamt':>10}")
print(f"{'='*70}")
for r in results:
print(f"{r['mitarbeiter']:<20} {r['wt_einheiten']:>6.1f} {r['we_gesamt']:>6.1f} {r['schwelle_erreicht']:<10} {r['auszahlung_gesamt']:>9.2f}")
print(f"{'='*70}")
if __name__ == "__main__":
if len(sys.argv) >= 2:
filepath = Path(sys.argv[1])
else:
filepath = Path("output/Dienstplan_2025_11_NRW.xlsx")
if not filepath.exists():
print(f"❌ Datei nicht gefunden: {filepath}")
sys.exit(1)
process_file(filepath)

89
src/fill_and_test.py Normal file
View file

@ -0,0 +1,89 @@
"""
Füllt das Plan-Blatt mit Datumswerten und fügt Testdaten hinzu (vereinfachte Version)
"""
from pathlib import Path
from datetime import date, timedelta
from openpyxl import load_workbook
import sys
def fill_plan_with_test_data(template_path, output_path, year, month):
"""Lädt die Vorlage, füllt Datum und fügt Testdaten ein."""
wb = load_workbook(template_path)
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)
# Testmitarbeiter
mitarbeiter = ["Max Mustermann", "Anna Schmidt", "Peter Klein"]
# Alle Tage durchgehen und mit Testdaten füllen
current_date = start_date
row = 2
while current_date <= end_date:
# Jeden 4. Tag: Split zwischen 2 Mitarbeitern
# Sonst: ein Mitarbeiter
if (row - 2) % 4 == 0:
# Split-Tag: 2 Einträge für denselben Tag
idx1 = (row - 2) % len(mitarbeiter)
idx2 = (idx1 + 1) % len(mitarbeiter)
# Erster Mitarbeiter
plan_ws[f"A{row}"] = current_date.strftime('%d.%m.%Y')
plan_ws[f"B{row}"] = mitarbeiter[idx1]
row += 1
# Zweiter Mitarbeiter (gleiches Datum)
plan_ws[f"A{row}"] = current_date.strftime('%d.%m.%Y')
plan_ws[f"B{row}"] = mitarbeiter[idx2]
row += 1
else:
# Normaler Tag: 1 Mitarbeiter
idx = (row - 2) % len(mitarbeiter)
plan_ws[f"A{row}"] = current_date.strftime('%d.%m.%Y')
plan_ws[f"B{row}"] = mitarbeiter[idx]
row += 1
current_date += timedelta(days=1)
wb.save(output_path)
print(f"✅ Plan vorbefüllt für {month:02d}/{year}")
print(f" Ausgabe: {output_path}")
print(f" Testdaten: {len(mitarbeiter)} Mitarbeiter für alle {row-2} Tage")
if __name__ == "__main__":
template = Path("templates/Dienstplan_Vorlage_V2_NRW_Simple.xlsx")
if len(sys.argv) >= 3:
year = int(sys.argv[1])
month = int(sys.argv[2])
else:
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_simple.py' aus!")
sys.exit(1)
fill_plan_with_test_data(template, output, year, month)

View file

@ -6,6 +6,7 @@ Nutzer muss nur noch Namen + Anteile eintragen.
from pathlib import Path from pathlib import Path
from datetime import date, timedelta from datetime import date, timedelta
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.styles import numbers
import sys import sys
@ -43,7 +44,9 @@ def fill_plan_with_dates(template_path, output_path, year, month):
row = 2 # Zeile 2 = erste Datenzeile nach Header row = 2 # Zeile 2 = erste Datenzeile nach Header
while current_date <= end_date: while current_date <= end_date:
plan_ws[f"A{row}"] = current_date cell = plan_ws[f"A{row}"]
cell.value = current_date
cell.number_format = 'DD.MM.YYYY' # Deutsches Datumsformat
# Spalten B (Mitarbeiter) und C (Anteil) bleiben leer zum Ausfüllen # Spalten B (Mitarbeiter) und C (Anteil) bleiben leer zum Ausfüllen
current_date += timedelta(days=1) current_date += timedelta(days=1)
row += 1 row += 1