Add support for avoiding shifts on specific dates with reasons
This commit is contained in:
@@ -4165,6 +4165,7 @@ class RotaBuilder(object):
|
||||
start_date: datetime.date | str | None = None,
|
||||
end_date: datetime.date | str | None = None,
|
||||
dates: Iterable[datetime.date] | None = None,
|
||||
reason: str | None = None,
|
||||
):
|
||||
"""Add a rule preventing the given workers from being assigned the given shifts
|
||||
on the provided date range or explicit dates.
|
||||
@@ -4190,7 +4191,7 @@ class RotaBuilder(object):
|
||||
sd = _coerce_date(start_date)
|
||||
ed = _coerce_date(end_date)
|
||||
|
||||
entry = {"names": list(names), "shifts": list(shifts), "start_date": sd, "end_date": ed, "dates": None}
|
||||
entry = {"names": list(names), "shifts": list(shifts), "start_date": sd, "end_date": ed, "dates": None, "reason": reason}
|
||||
if dates is not None:
|
||||
entry["dates"] = [d if isinstance(d, datetime.date) else _coerce_date(d) for d in dates]
|
||||
|
||||
@@ -5104,6 +5105,49 @@ class RotaBuilder(object):
|
||||
):
|
||||
model = self.model
|
||||
|
||||
# Build a lookup of per-worker/day avoid-shift rules (worker.id, week, day) -> {shifts: [...], reason: str}
|
||||
avoid_lookup: dict[tuple, dict] = {}
|
||||
try:
|
||||
for entry in getattr(self.constraint_options_model, "avoid_shifts_by_worker_dates", []):
|
||||
names = entry.get("names", []) or []
|
||||
shifts = entry.get("shifts", []) or []
|
||||
reason = entry.get("reason")
|
||||
dates = entry.get("dates")
|
||||
sd = entry.get("start_date")
|
||||
ed = entry.get("end_date")
|
||||
|
||||
date_list = []
|
||||
if dates:
|
||||
date_list = dates
|
||||
elif sd is not None or ed is not None:
|
||||
try:
|
||||
for d in self.get_date_range(sd, ed):
|
||||
date_list.append(d)
|
||||
except Exception:
|
||||
# fall back to trying to iterate
|
||||
pass
|
||||
|
||||
for name in names:
|
||||
matching_workers = [w for w in self.workers if w.name.strip() == name.strip()]
|
||||
if not matching_workers:
|
||||
continue
|
||||
wid = matching_workers[0].id
|
||||
for d in date_list:
|
||||
try:
|
||||
wk, day = self.get_week_day_by_date(d)
|
||||
except Exception:
|
||||
continue
|
||||
key = (wid, wk, day)
|
||||
existing = avoid_lookup.get(key, {"shifts": [], "reason": None})
|
||||
existing["shifts"].extend(shifts)
|
||||
# preserve reason if provided
|
||||
if reason:
|
||||
existing["reason"] = reason
|
||||
avoid_lookup[key] = existing
|
||||
except Exception:
|
||||
# If constraint options not present or parsing fails, continue without avoid info
|
||||
avoid_lookup = {}
|
||||
|
||||
timetable = []
|
||||
|
||||
if self.run_start_time is not None and self.run_end_time is not None:
|
||||
@@ -5252,6 +5296,16 @@ class RotaBuilder(object):
|
||||
if locum:
|
||||
css_class = " ".join((css_class, "locum-shift"))
|
||||
|
||||
# Add visual marker/data if this day/worker has avoid-shift rules
|
||||
avoid_info = avoid_lookup.get((worker.id, week, day))
|
||||
avoid_attr = ""
|
||||
if avoid_info is not None:
|
||||
# mark the cell as having avoids; include which shifts and the reason
|
||||
avoid_shifts = ",".join(avoid_info.get("shifts", []))
|
||||
avoid_reason = avoid_info.get("reason") or ""
|
||||
css_class = " ".join((css_class, "avoid-shift"))
|
||||
avoid_attr = f" data-avoid-shifts='{avoid_shifts}' data-avoid-reason='{avoid_reason}'"
|
||||
|
||||
remote_site = ""
|
||||
if shift_name:
|
||||
shift = self.get_shift_by_name(shift_name)
|
||||
@@ -5267,7 +5321,7 @@ class RotaBuilder(object):
|
||||
f" data-available='{available}'"
|
||||
f" data-unavailable_reason='{unavailable_reason}'"
|
||||
f" data-date='{d}' data-week='{week}' data-day='{day}'"
|
||||
f"{remote_site}{requests}{bank_holiday}>{shift_display_char}</td>"
|
||||
f"{remote_site}{requests}{bank_holiday}{avoid_attr}>{shift_display_char}</td>"
|
||||
)
|
||||
|
||||
shift_count = ""
|
||||
|
||||
@@ -117,6 +117,7 @@ class AvoidShiftOnDates(BaseModel):
|
||||
start_date: Optional[datetime.date] = None
|
||||
end_date: Optional[datetime.date] = None
|
||||
dates: Optional[list[datetime.date]] = None
|
||||
reason: Optional[str] = None
|
||||
|
||||
@field_validator("start_date", "end_date", mode="before")
|
||||
def coerce_dates(cls, v):
|
||||
@@ -428,6 +429,7 @@ class Worker(BaseModel):
|
||||
"start_date": av.start_date,
|
||||
"end_date": av.end_date,
|
||||
"dates": av.dates,
|
||||
"reason": getattr(av, "reason", None),
|
||||
}
|
||||
# append to rota-level list; the model builder will enforce these
|
||||
if hasattr(Rota.constraint_options_model, "avoid_shifts_by_worker_dates"):
|
||||
|
||||
Reference in New Issue
Block a user