.
This commit is contained in:
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user