Mehr Fuktionen fuer historische Dienste
Planungszeitraum und historischer Zeitraum koennen sich jetzt ueberlappen. So lassen sich einzelne Dienste (z.B. von einer Familie) nachtraeglich neu planen. historische Dienste werden bei den Constraints 1 Dienst pro Tag und 1 Dienst pro Woche korrekt beruecksichtigt elterndienstplaner.py erzeugt jetzt ausgaben-gesamt.csv, die fuer spaetere Aufrufe als Eingabe vorherige-dienste.csv verwendet werden kann.
This commit is contained in:
parent
b524edc2ba
commit
08e5cf11bd
@ -54,17 +54,23 @@ Leon,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0
|
||||
|
||||
### vorherige-ausgaben.csv (optional)
|
||||
|
||||
Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv`.
|
||||
Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv` bzw. `ausgabe-gesamt.csv`.
|
||||
Hier kann die `ausgabe-gesamt.csv`, die bei der letzten Planung generiert wurde eingespielt werden.
|
||||
|
||||
## Ausgabedatei
|
||||
|
||||
### ausgabe.csv
|
||||
|
||||
Die neu zugeteilten Dienste.
|
||||
```csv
|
||||
Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
|
||||
2026-01-06,Montag,Sarah & Tim,Leon,Erika,,
|
||||
```
|
||||
|
||||
|
||||
### ausgabe-gesamt.csv
|
||||
Wie `ausgabe.csv`, enthält aber neben den neu geplanten Diensten auch die historischen Dienste, die über `vorherige-ausgaben.csv` übergeben wurden. Die Datei `ausgabe-gesamt.csv` kann bei der nächsten Planung wieder als Eingabe `vorherige-ausgaben.csv` verwendet werden.
|
||||
|
||||
## Verwendung
|
||||
|
||||
```bash
|
||||
|
||||
14
ausgabe.py
14
ausgabe.py
@ -6,7 +6,7 @@ Visualisierung und Export der Ergebnisse
|
||||
|
||||
from datetime import date
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, DefaultDict
|
||||
from typing import Dict, List, DefaultDict, Tuple
|
||||
|
||||
from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung
|
||||
from csv_io import AusgabeWriter
|
||||
@ -20,6 +20,8 @@ class ElterndienstAusgabe:
|
||||
# Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt)
|
||||
self.ziel_lokal: Zielverteilung = None
|
||||
self.ziel_global: Zielverteilung = None
|
||||
# Historische Dienste (kann über Observer gesetzt werden)
|
||||
self.historische_dienste: List[Tuple[date, Eltern, Dienst]] = None
|
||||
|
||||
def setze_zielverteilungen(
|
||||
self,
|
||||
@ -30,9 +32,17 @@ class ElterndienstAusgabe:
|
||||
self.ziel_lokal = ziel_lokal
|
||||
self.ziel_global = ziel_global
|
||||
|
||||
def setze_historische_dienste(self, historische_dienste: List[Tuple[date, Eltern, Dienst]]) -> None:
|
||||
"""Observer-Callback: Setzt historische Dienste für Ausgabe/Export"""
|
||||
self.historische_dienste = historische_dienste
|
||||
|
||||
def schreibe_ausgabe_csv(self, datei: str, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None:
|
||||
"""Schreibt die Lösung in die ausgabe.csv"""
|
||||
AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste)
|
||||
AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste, False)
|
||||
# Schreibe ergänzende Datei mit historischen Diensten (falls vorhanden).
|
||||
hist_datei = datei.replace('.csv', '-gesamt.csv') if datei.endswith('.csv') else datei + '-gesamt.csv'
|
||||
historische = self.historische_dienste if self.historische_dienste is not None else self.daten.historische_dienste
|
||||
AusgabeWriter.schreibe_ausgabe_csv(hist_datei, lösung, self.daten.planungszeitraum, self.daten.dienste, True, historische)
|
||||
|
||||
def drucke_statistiken(self, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None:
|
||||
"""Druckt Statistiken zur Lösung"""
|
||||
|
||||
57
csv_io.py
57
csv_io.py
@ -6,7 +6,7 @@ Trennt CSV-Parsing und -Schreiben von der Business-Logik
|
||||
|
||||
import csv
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import Dict, List, Tuple, DefaultDict
|
||||
from typing import Dict, List, Tuple, DefaultDict, Optional
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@ -213,7 +213,9 @@ class AusgabeWriter:
|
||||
datei: str,
|
||||
lösung: Dict[date, Dict[any, List[str]]], # Dienst-Objekt als Key
|
||||
tage: List[date],
|
||||
dienste: List # List[Dienst]
|
||||
dienste: List, # List[Dienst]
|
||||
gesamt: bool = False,
|
||||
historische_dienste: List[Tuple[date, str, any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Schreibt die Lösung in die ausgabe.csv
|
||||
@ -221,10 +223,50 @@ class AusgabeWriter:
|
||||
Args:
|
||||
datei: Pfad zur ausgabe.csv
|
||||
lösung: Dictionary mit Zuteilungen {datum: {dienst: [eltern]}}
|
||||
tage: Liste aller Planungstage
|
||||
tage: Liste aller Planungstage (aktueller Planungszeitraum)
|
||||
dienste: Liste der Dienst-Objekte
|
||||
gesamt: Wenn True -> schreibe gesamte Dienste (inkl. historische_dienste).
|
||||
Wenn False -> wie bisher nur die neu verplanten Dienste (lösung).
|
||||
historische_dienste: Optional Liste von (datum, eltern, dienst) aus vorherige-ausgaben.csv
|
||||
(wird nur ausgewertet, wenn gesamt==True)
|
||||
"""
|
||||
print(f"Schreibe Ergebnisse nach {datei}...")
|
||||
print(f"Schreibe Ergebnisse nach {datei}... (gesamt={gesamt})")
|
||||
|
||||
# Bestimme alle zu schreibenden Tage
|
||||
if gesamt and historische_dienste:
|
||||
historische_dates = {hd[0] for hd in historische_dienste}
|
||||
output_dates = sorted(set(tage) | historische_dates)
|
||||
else:
|
||||
output_dates = sorted(tage)
|
||||
|
||||
# Sicherstellen: Für alle Tage im Zeitraum (von min bis max) soll eine Zeile ausgegeben werden,
|
||||
# auch wenn keine Informationen vorliegen.
|
||||
if output_dates:
|
||||
start_date = min(output_dates)
|
||||
end_date = max(output_dates)
|
||||
full_dates = []
|
||||
current = start_date
|
||||
while current <= end_date:
|
||||
full_dates.append(current)
|
||||
current += timedelta(days=1)
|
||||
output_dates = full_dates
|
||||
|
||||
# Erstelle Mapping date -> dienst -> list[eltern]
|
||||
combined: Dict[date, Dict[any, List[str]]] = {}
|
||||
if gesamt and historische_dienste:
|
||||
for datum, eltern_name, dienst in historische_dienste:
|
||||
combined.setdefault(datum, {}).setdefault(dienst, [])
|
||||
if eltern_name not in combined[datum][dienst]:
|
||||
combined[datum][dienst].append(eltern_name)
|
||||
|
||||
# Füge neue (optimierte) Zuweisungen hinzu (überschreiben/ergänzen)
|
||||
for datum, dienst_map in (lösung or {}).items():
|
||||
combined.setdefault(datum, {})
|
||||
for dienst, eltern_liste in dienst_map.items():
|
||||
combined[datum].setdefault(dienst, [])
|
||||
for eltern_name in eltern_liste:
|
||||
if eltern_name not in combined[datum][dienst]:
|
||||
combined[datum][dienst].append(eltern_name)
|
||||
|
||||
with open(datei, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
@ -234,17 +276,16 @@ class AusgabeWriter:
|
||||
writer.writerow(header)
|
||||
|
||||
# Daten schreiben
|
||||
for tag in sorted(tage):
|
||||
for tag in output_dates:
|
||||
wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',
|
||||
'Freitag', 'Samstag', 'Sonntag'][tag.weekday()]
|
||||
|
||||
row = [tag.strftime('%Y-%m-%d'), wochentag]
|
||||
|
||||
for dienst in dienste:
|
||||
if tag in lösung and dienst in lösung[tag]:
|
||||
eltern_str = ' und '.join(lösung[tag][dienst])
|
||||
else:
|
||||
eltern_str = ''
|
||||
if tag in combined and dienst in combined[tag]:
|
||||
eltern_str = ' und '.join(combined[tag][dienst])
|
||||
row.append(eltern_str)
|
||||
|
||||
writer.writerow(row)
|
||||
|
||||
@ -102,11 +102,16 @@ class ElterndienstplanerDaten:
|
||||
vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints
|
||||
"""
|
||||
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
|
||||
self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \
|
||||
EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst)
|
||||
|
||||
# Eltern CSV: Dienstfaktoren
|
||||
# Eltern CSV: Dienstfaktoren (erst einlesen, damit self.eltern daraus abgeleitet wird)
|
||||
self.dienstfaktoren = EingabeParser.parse_eltern_csv(eltern_datei)
|
||||
# Fülle self.eltern aus den Einträgen in eltern.csv (Vertrauensquelle für Elternnamen)
|
||||
self.eltern = list(self.dienstfaktoren.keys())
|
||||
|
||||
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
|
||||
# Wir verwenden die Elterndefinition aus eltern.csv; die von parse_eingabe_csv
|
||||
# zurückgegebene Eltern-Liste wird ignoriert, damit die Quell-of-truth konsistent bleibt.
|
||||
_, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \
|
||||
EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst)
|
||||
|
||||
# Vorherige Ausgaben CSV (optional): Historische Dienste für Fairness
|
||||
if vorherige_datei:
|
||||
|
||||
@ -179,6 +179,10 @@ class Elterndienstplaner:
|
||||
woche_nr = 0
|
||||
letzter_tag = self.daten.planungszeitraum[-1]
|
||||
|
||||
print ("\n Erster Tag im Planungszeitraum:", erster_tag)
|
||||
print ("\n Letzter Tag im Planungszeitraum:", letzter_tag)
|
||||
|
||||
|
||||
while woche_start <= letzter_tag:
|
||||
woche_ende = woche_start + timedelta(days=6)
|
||||
|
||||
@ -188,11 +192,12 @@ class Elterndienstplaner:
|
||||
|
||||
# Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum)
|
||||
historische_dienste_in_woche = 0
|
||||
if woche_start < erster_tag:
|
||||
#if woche_start < erster_tag:
|
||||
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
|
||||
if (hist_eltern == eltern and
|
||||
hist_dienst == dienst and
|
||||
woche_start <= hist_datum < erster_tag):
|
||||
woche_start <= hist_datum and
|
||||
hist_datum <= woche_ende):
|
||||
historische_dienste_in_woche += 1
|
||||
|
||||
for tag in self.daten.planungszeitraum:
|
||||
@ -201,8 +206,11 @@ class Elterndienstplaner:
|
||||
woche_vars.append(x[eltern, tag, dienst])
|
||||
|
||||
if woche_vars:
|
||||
if (1 - historische_dienste_in_woche) >= 0:
|
||||
prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \
|
||||
f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}"
|
||||
else:
|
||||
print (f" Hinweis: {eltern} hat in Woche {woche_nr} bereits {historische_dienste_in_woche} mal {dienst.name}")
|
||||
|
||||
woche_start += timedelta(days=7)
|
||||
woche_nr += 1
|
||||
@ -216,12 +224,18 @@ class Elterndienstplaner:
|
||||
for eltern in self.daten.eltern:
|
||||
for tag in self.daten.planungszeitraum:
|
||||
tag_vars = []
|
||||
maximum = 1
|
||||
for dienst in self.daten.dienste:
|
||||
if (eltern, tag, dienst) in x:
|
||||
tag_vars.append(x[eltern, tag, dienst])
|
||||
|
||||
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
|
||||
if (hist_eltern == eltern and
|
||||
hist_datum == tag):
|
||||
maximum = 0
|
||||
|
||||
if tag_vars:
|
||||
prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}"
|
||||
prob += pulp.lpSum(tag_vars) <= maximum, f"C2_{eltern.replace(' ', '_')}_{tag}"
|
||||
|
||||
def _add_constraint_verfuegbarkeit(
|
||||
self,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user