diff --git a/src/add_test_data.py b/src/add_test_data.py new file mode 100644 index 0000000..1389f97 --- /dev/null +++ b/src/add_test_data.py @@ -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) diff --git a/src/build_template.py b/src/build_template.py index 7127147..e62a4f7 100644 --- a/src/build_template.py +++ b/src/build_template.py @@ -4,7 +4,7 @@ from pathlib import Path from datetime import date 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.table import Table, TableStyleInfo from openpyxl.utils import get_column_letter @@ -103,7 +103,10 @@ def _populate_holidays(ws): all_holidays = NRW_HOLIDAYS_2025 + NRW_HOLIDAYS_2026 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 tab = Table(displayName="tblFeiertage", ref=f"A1:C{len(all_holidays)+1}") @@ -116,6 +119,10 @@ def _populate_holidays(ws): ws.column_dimensions["B"].width = 32 ws.column_dimensions["C"].width = 8 _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: @@ -124,18 +131,18 @@ def _plan_formulas(row: int) -> dict: 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' + holiday_check = f'SUMPRODUCT((tblFeiertage[Datum]={date_cell})*(tblFeiertage[BL]=Regeln!$B$6))>0' + vortag_check = f'SUMPRODUCT((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 + "D": f"=IFERROR({holiday_check},FALSE)", # Ist_FEIERTAG + "E": f"=IFERROR({vortag_check},FALSE)", # Ist_VORTAG + "F": f"=IFERROR(WEEKDAY({date_cell},2)=5,FALSE)", # Ist_Freitag + "G": f"=OR($F{row},WEEKDAY({date_cell},2)=6,WEEKDAY({date_cell},2)=7,$D{row},$E{row})", # Ist_WE_Tag + "H": f"=NOT($G{row})", # Ist_WT_Tag + "I": f"=IF($H{row},{anteil_cell},0)", # WT_Einheit + "J": f"=IF(AND($G{row},$F{row}),{anteil_cell},0)", # WE_Freitag_Einheit + "K": f"=IF(AND($G{row},NOT($F{row})),{anteil_cell},0)", # WE_Andere_Einheit } @@ -170,6 +177,10 @@ def _populate_plan(ws): ws.column_dimensions["C"].width = 10 for col in "DEFGHIJK": 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): @@ -185,18 +196,18 @@ def _populate_auswertung(ws): # Row 2 onwards: formulas reference column A monat_start = "Regeln!$B$7" - monat_end = f"MONATSENDE({monat_start};0)" + monat_end = f"EOMONTH({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}="";""' + guard = f'IF({name_ref}="",""' - # WT_Einheiten - using SUMMENPRODUKT for compatibility + # WT_Einheiten - using SUMPRODUCT for compatibility 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[WT_Einheit])))' ) @@ -204,7 +215,7 @@ def _populate_auswertung(ws): # WE_Freitag 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[WE_Freitag_Einheit])))' ) @@ -212,38 +223,38 @@ def _populate_auswertung(ws): # WE_Andere 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[WE_Andere_Einheit])))' ) ws[f"D{row}"] = we_other_formula # WE_Gesamt - ws[f"E{row}"] = f'={guard};C{row}+D{row})' + 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"))' + ws[f"F{row}"] = f'={guard},IF(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))' + ws[f"G{row}"] = f'={guard},IF(E{row}>=Regeln!$B$4-0.0001,Regeln!$B$5,0))' # 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 - 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) - ws[f"J{row}"] = f'={guard};WENN(E{row}= 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) diff --git a/src/fill_and_test.py b/src/fill_and_test.py new file mode 100644 index 0000000..e89f3d3 --- /dev/null +++ b/src/fill_and_test.py @@ -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) diff --git a/src/fill_plan_dates.py b/src/fill_plan_dates.py index fcb07ce..8636113 100644 --- a/src/fill_plan_dates.py +++ b/src/fill_plan_dates.py @@ -6,6 +6,7 @@ Nutzer muss nur noch Namen + Anteile eintragen. from pathlib import Path from datetime import date, timedelta from openpyxl import load_workbook +from openpyxl.styles import numbers 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 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 current_date += timedelta(days=1) row += 1