Add support for avoiding shifts on specific dates for workers

- Introduced AvoidShiftOnDates model to define constraints for workers.
- Enhanced Worker class to register avoid-shift rules with the rota builder.
- Implemented logic in RotaBuilder to enforce these constraints during shift assignment.
- Added tests to verify functionality for avoiding shifts on single dates and date ranges.
This commit is contained in:
Ross
2026-01-05 21:44:34 +00:00
parent 62e48ef205
commit e6bdd4a09e
6 changed files with 289 additions and 3 deletions
+40
View File
@@ -111,6 +111,29 @@ class OutOfProgramme(BaseModel):
reason: str = ""
class AvoidShiftOnDates(BaseModel):
"""Worker-level constraint: avoid listed shifts on given dates or date range."""
shifts: list[str]
start_date: Optional[datetime.date] = None
end_date: Optional[datetime.date] = None
dates: Optional[list[datetime.date]] = None
@field_validator("start_date", "end_date", mode="before")
def coerce_dates(cls, v):
# allow strings in common formats
if v is None:
return None
if isinstance(v, datetime.date):
return v
if isinstance(v, str):
for fmt in ("%d/%m/%Y", "%d/%m/%y", "%Y-%m-%d"):
try:
return datetime.datetime.strptime(v, fmt).date()
except Exception:
continue
raise ValueError(f"Cannot parse date: {v}")
class Worker(BaseModel):
name: str
site: str
@@ -154,6 +177,7 @@ class Worker(BaseModel):
shift_fte_overrides: dict[str, int] = {} # Need checks to ensure shifts exist
weekend_shift_target_number: int = 0
avoid_shifts_on_dates: list[AvoidShiftOnDates] = []
@@ -395,6 +419,22 @@ class Worker(BaseModel):
style="red",
)
# Register any per-worker avoid-shift-on-dates rules with the rota builder
try:
for av in getattr(self, "avoid_shifts_on_dates", []):
entry = {
"names": [self.name],
"shifts": av.shifts,
"start_date": av.start_date,
"end_date": av.end_date,
"dates": av.dates,
}
# append to rota-level list; the model builder will enforce these
if hasattr(Rota.constraint_options_model, "avoid_shifts_by_worker_dates"):
Rota.constraint_options_model.avoid_shifts_by_worker_dates.append(entry)
except Exception:
Rota.add_warning("Worker/avoid_shifts_on_dates", f"Failed to register avoid_shifts_on_dates for {self.name}")
def __lt__(self, other) -> bool:
return (self.site, self.grade, self.fte_adj, self.name) < (
other.site,