This commit is contained in:
Ross
2025-12-17 11:27:16 +00:00
parent bf3b685923
commit a347ad7777
2 changed files with 178 additions and 7 deletions
+82 -7
View File
@@ -351,6 +351,7 @@ class RotaConstraintOptions(BaseModel):
balance_weekend_days: bool = Field(False, description="Balance weekend days (Sat/Sun) separately")
balance_weekend_days_weight: int = Field(5, description="Objective weight for balancing weekend days per day")
balance_weekend_days_quadratic: bool = Field(False, description="Use quadratic penalty to balance weekend days per day")
balance_weekend_days_use_previous_shifts: bool = Field(False, description="When true, adjust per-day weekend (Sat/Sun) targets using worker.previous_shifts (allocated-worked) distributed per shift day")
weekend_day_hard_max_deviation: Optional[int] = Field(
None,
description=(
@@ -2194,10 +2195,36 @@ class RotaBuilder(object):
if self.use_previous_shifts:
if shift.name in worker.previous_shifts:
worked, allocated = worker.previous_shifts[shift.name]
target_shifts = (
target_shifts + float(allocated) - float(worked)
)
entry = worker.previous_shifts[shift.name]
# entry may be a tuple (worked, allocated) or a dict of per-day tuples
if isinstance(entry, dict):
# prefer overall tuple if present
if "__all__" in entry:
try:
worked = float(entry["__all__"][0])
allocated = float(entry["__all__"][1])
target_shifts = target_shifts + allocated - worked
except Exception:
pass
else:
# sum any per-day entries
try:
total_worked = 0.0
total_alloc = 0.0
for v in entry.values():
total_worked += float(v[0])
total_alloc += float(v[1])
target_shifts = target_shifts + total_alloc - total_worked
except Exception:
pass
else:
try:
worked, allocated = entry
target_shifts = (
target_shifts + float(allocated) - float(worked)
)
except Exception:
pass
if self.use_shift_balance_extra:
if shift.name in worker.shift_balance_extra:
@@ -2533,6 +2560,41 @@ class RotaBuilder(object):
if day_name in shift.days
)
# Optionally adjust per-day weekend targets using previous_shifts
if self.use_previous_shifts and self.constraint_options_model.balance_weekend_days_use_previous_shifts:
# For each shift that includes this day, if previous_shifts contains an entry
# add (allocated - worked) / len(shift.days) to the per-day target
prev_adj = 0.0
for shift in self.get_shifts():
if day_name in shift.days and shift.name in getattr(worker, "previous_shifts", {}):
entry = worker.previous_shifts.get(shift.name)
# support per-day nested dicts: { 'Sat': (worked, allocated), 'Sun': (...) }
if isinstance(entry, dict):
# prefer explicit day entry if present
if day_name in entry:
try:
worked = float(entry[day_name][0])
allocated = float(entry[day_name][1])
prev_adj += (allocated - worked)
except Exception:
continue
# fallback: if an overall tuple was preserved under '__all__', use distributed value
elif "__all__" in entry:
try:
worked = float(entry["__all__"][0])
allocated = float(entry["__all__"][1])
prev_adj += (allocated - worked) / max(1, len(shift.days))
except Exception:
continue
else:
try:
worked = float(entry[0])
allocated = float(entry[1])
prev_adj += (allocated - worked) / max(1, len(shift.days))
except Exception:
continue
weekend_day_shift_target_number = weekend_day_shift_target_number + prev_adj
self.model.constraints.add(
self.model.weekend_day_shift_count_t1[worker.id, day_name]
- self.model.weekend_day_shift_count_t2[worker.id, day_name]
@@ -4904,12 +4966,25 @@ class RotaBuilder(object):
for shift in self.get_shifts():
prev = getattr(worker, "previous_shifts", {}) or {}
if shift.name in prev:
worked, allocated = prev[shift.name]
entry = prev[shift.name]
# entry may be a tuple (worked, allocated) or a dict with per-day entries
if isinstance(entry, dict):
# prefer a day-specific entry if available (variable `d` holds the day name in scope)
if d in entry:
worked, allocated = entry[d]
elif "__all__" in entry:
worked, allocated = entry["__all__"]
else:
# fallback: sum any tuple values found in the dict
worked = sum(v[0] for v in entry.values() if isinstance(v, (list, tuple)))
allocated = sum(v[1] for v in entry.values() if isinstance(v, (list, tuple)))
else:
worked, allocated = entry
try:
adj = float(allocated) - float(worked)
except Exception:
adj = 0.0
prior_map[shift.name] = {"worked": worked, "allocated": allocated, "adjustment": adj}
adj = 0
prior_map[shift.name] = dict(worked=worked, allocated=allocated, adjustment=adj)
if adj != 0:
adjustments_list.append(f"{shift.name}:{adj:+g}")