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:
parent
35de2c27f0
commit
034b398c2c
6 changed files with 571 additions and 30 deletions
69
src/add_test_data.py
Normal file
69
src/add_test_data.py
Normal 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)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
120
src/build_template_simple.py
Normal file
120
src/build_template_simple.py
Normal 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
249
src/calculate.py
Normal 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
89
src/fill_and_test.py
Normal 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)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Reference in a new issue