many improvements

This commit is contained in:
Ross
2022-07-11 19:46:40 +01:00
parent 0cebbfddf6
commit dc975b991c
25 changed files with 1949 additions and 863 deletions
+319 -145
View File
@@ -1,9 +1,9 @@
from calendar import week
import datetime
import itertools
from typing import Dict, Iterable, List, Sequence, Tuple, Set
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type
from pydantic import BaseModel, Extra
from pydantic import BaseModel, Extra, constr
from pathlib import Path
@@ -47,6 +47,11 @@ SHIFT_BOUNDS = {
}
class ShiftConstraint(BaseModel):
name: str
options: bool | int | Dict | tuple = False
class SingleShift(BaseModel):
"""Class to hold all details for a shift
@@ -70,66 +75,75 @@ class SingleShift(BaseModel):
"""
sites: Iterable[str]
sites: List[str]
name: str
length: float
shift_days: Sequence[str]
balance_offset: float | None = None,
balance_weighting: float = 1,
workers_required: float = 1,
rota_on_nwds: bool = False,
assign_as_block: bool = False,
force_as_block: bool = False,
force_as_block_unless_nwd: bool = False,
hard_constrain_shift: bool = True,
bank_holidays_only: bool = False,
constraint: Iterable[str] = [],
days: str | List[str]
balance_offset: float | None = None
balance_weighting: float = 1
workers_required: float = 1
rota_on_nwds: bool = False
assign_as_block: bool = False
force_as_block: bool = False
force_as_block_unless_nwd: bool = False
hard_constrain_shift: bool = True
bank_holidays_only: bool = False
constraint: List[ShiftConstraint] = []
class Config:
extra = Extra.allow
orm_mode = True
def __init__(self, **data: Any):
super().__init__(**data)
#self.site = sites
#self.name = name
#self.length = length
#self.shift_days = shift_days
# self.site = sites
# self.name = name
# self.length = length
# self.days = days
# self.balance_by_site = balance_by_site
# balance_offset defines the max difference in allocated shifts
# versus target shifts (hard constraint)
# if 0 exactly equal shifts must be assigend (unlikely to be possible)
if self.balance_offset is None:
self.balance_offset = len(self.shift_days)
self.balance_offset = len(self.days)
if self.force_as_block_unless_nwd:
self.assign_as_block = True
#self.assign_as_block = assign_as_block
# self.assign_as_block = assign_as_block
self.constraint_options = {}
self.constraints = []
for c in self.constraint:
match c:
case (constraint, options):
self.constraints.append(constraint)
self.constraint_options[constraint] = options
case constraint:
self.constraints.append(constraint)
self.constraints.append(c.name)
if c.options:
self.constraint_options[c.name] = c.options
# constraint = c.constraint
# match c:
# case (constraint, options):
# self.constraint_options[constraint] = options
# case constraint:
# self.constraints.append(constraint)
if isinstance(self.days, str):
self.days = [self.days]
def __str__(self):
return f"name: {self.name}, site: {self.site}, length: {self.length}, balance weighting: {self.balance_weighting}, balance offset: {self.balance_offset}, hard_constrain: {self.hard_constrain_shift}, workers required : {self.workers_required}, rota on nwds {self.rota_on_nwds}, assign as block: {self.assign_as_block}, force as block: {self.force_as_block}, constraints: {self.constraints}, constraint_options: {self.constraint_options}" # , total shifts: {self.total_shifts}"
return f"name: {self.name}, site: {self.sites}, length: {self.length}, balance weighting: {self.balance_weighting}, balance offset: {self.balance_offset}, hard_constrain: {self.hard_constrain_shift}, workers required : {self.workers_required}, rota on nwds {self.rota_on_nwds}, assign as block: {self.assign_as_block}, force as block: {self.force_as_block}, constraints: {self.constraints}, constraint_options: {self.constraint_options}" # , total shifts: {self.total_shifts}"
def get_shift_summary(self):
"""
Returns a text summary of the shift
"""
return f"{self.name}: {self.workers_required} workers for sites ({', '.join(self.site)}) on days ({', '.join(self.shift_days)})"
return f"{self.name}: {self.workers_required} workers for sites ({', '.join(self.site)}) on days ({', '.join(self.days)})"
def get_shift_number(self):
return len(self.days)
class RotaBuilder(object):
@@ -137,16 +151,14 @@ class RotaBuilder(object):
def __init__(
self,
start_date: datetime.datetime = datetime.datetime.now(),
start_date: datetime.date = datetime.date.today(),
weeks_to_rota: int = 26,
balance_offset_modifier: int = 1,
ltft_balance_offset: int = 1,
max_night_frequency: int = 2,
max_weekend_frequency: int = 1, # Requires balance weekends
use_previous_shifts: bool = False,
use_shift_balance_extra: bool = False,
use_bank_holiday_extra: bool = False,
SHIFT_BOUNDS = SHIFT_BOUNDS
SHIFT_BOUNDS=SHIFT_BOUNDS,
):
print(f"Start time: {datetime.datetime.now()}")
@@ -155,7 +167,7 @@ class RotaBuilder(object):
print(f"Use previous shifts (balance_extra) {use_shift_balance_extra}")
self.SHIFT_BOUNDS = SHIFT_BOUNDS
self.shifts = [] # type List[SingleShift]
self.shifts: List[SingleShift] = [] # type List[SingleShift]
self.days = days
@@ -180,7 +192,7 @@ class RotaBuilder(object):
self.unavailable_to_work_reason = {}
self.pref_not_to_work = {}
self.pref_not_to_work_reason = {}
self.work_requests = set()
self.work_requests: set[Tuple[str, WeekInt, DayStr, ShiftName]] = set()
self.work_requests_map = {}
self.workers: list[Worker] = []
@@ -193,9 +205,6 @@ class RotaBuilder(object):
self.balance_offset_modifier = balance_offset_modifier
self.ltft_balance_offset = ltft_balance_offset
# Don't assign multiple shifts every (n) weeks
self.max_night_frequency = max_night_frequency
self.max_weekend_frequency = max_weekend_frequency
self.constraint_options = {
"balance_nights": True,
@@ -204,21 +213,26 @@ class RotaBuilder(object):
"balance_bank_holidays": True,
"balance_blocks": True,
"balance_shifts": True, # Does not use a quadratic function
"balance_shifts_quadratic": False, # Will prevent spreading of spreading across different shifts
"balance_shifts_quadratic": False, # Will prevent spreading of spreading across different shifts
"balance_shifts_over_workers": True,
"minimise_shift_diffs": False, # less sophisticated version of balance_shifts_over_workers
"balance_weekends": True,
"max_weekends": 100,
# Don't assign multiple shifts every (n) weeks
"max_weekend_frequency": 1, # Requires balance weekends
# Don't assign multiple shifts every (n) weeks
"max_night_frequency": 4,
"max_shifts_per_week": 7,
"max_shifts_per_month": 40,
# The following will cause an unsolvable problem if a shift
# is forced as a block and spans the time peroid
"prevent_monday_after_full_weekends": [],
"prevent_monday_and_tuesday_after_full_weekends": [],
"prevent_fridays_before_full_weekends": [],
"prevent_fridays_before_full_weekends": [],
"prevent_thursdays_before_full_weekends": [],
"avoid_st2_first_month": False,
"hard_constrain_pair_separation": False,
"avoid_shifts_by_grades": [],
}
# Generate a map for week, day combinations to a given date
@@ -263,7 +277,9 @@ class RotaBuilder(object):
if not results.solver.status:
sys.exit(0)
def build_and_solve(self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True):
def build_and_solve(
self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True
):
self.build_shifts()
self.build_workers()
self.build_model()
@@ -611,11 +627,22 @@ class RotaBuilder(object):
initialize=availability_init,
)
work_request_sets = set()
for work_request in self.work_requests:
if work_request[3] == "*":
pass
else:
work_request_sets.add(work_request[3])
# Validate shifts are valid
work_request_sets = set([i[3] for i in self.work_requests])
#work_request_sets = set([i[3] for i in self.work_requests])
invalid_shifts = work_request_sets - set(self.get_shift_names())
if invalid_shifts:
raise InvalidShift(f"Invalid worker shift request [shift(s): ${invalid_shifts}]")
raise InvalidShift(
f"Invalid worker shift request [shift(s): ${invalid_shifts}]"
)
def work_request_init(model, wid, week, day, shift):
if (wid, week, day, shift) in self.work_requests:
@@ -692,7 +719,6 @@ class RotaBuilder(object):
# self.model.works[worker.id, week, day, shift]
# for worker in self.workers if worker.site not in site_required))
# Constraint: total hours worked hours worked
# for worker in self.workers:
# self.model.constraints.add(
@@ -735,8 +761,7 @@ class RotaBuilder(object):
return Constraint.Skip
return (
sum(
model.shift_week_worker_assigned[shift.name, week, w.id]
for w in workers
model.shift_week_worker_assigned[shift, week, w.id] for w in workers
)
<= limit
)
@@ -754,7 +779,7 @@ class RotaBuilder(object):
Constraint(
[week for week in self.weeks],
# [shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
[shift],
[shift.name],
[grade],
[shift.constraint_options["limit_grade_number"][grade]],
rule=limitGradesRule,
@@ -769,8 +794,7 @@ class RotaBuilder(object):
raise ValueError("minimum_grade_number: not enough valid workers")
return (
sum(
model.shift_week_worker_assigned[shift.name, week, w.id]
for w in workers
model.shift_week_worker_assigned[shift, week, w.id] for w in workers
)
>= limit
)
@@ -788,7 +812,7 @@ class RotaBuilder(object):
Constraint(
[week for week in self.weeks],
# [shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
[shift],
[shift.name],
[grade],
[min_required],
rule=minimumGradesRule,
@@ -796,7 +820,9 @@ class RotaBuilder(object):
),
)
def presenceAtRemoteSiteWeek(model, week, shift, required_site, required_number):
def presenceAtRemoteSiteWeek(
model, week, shift, required_site, required_number
):
required_site_workers = [
w for w in self.workers if required_site == w.remote_site
]
@@ -809,7 +835,9 @@ class RotaBuilder(object):
>= required_number
)
for shift in self.get_shifts_with_constraint("require_remote_site_presence_week"):
for shift in self.get_shifts_with_constraint(
"require_remote_site_presence_week"
):
site, required_number = shift.constraint_options[
"require_remote_site_presence_week"
]
@@ -826,7 +854,9 @@ class RotaBuilder(object):
),
)
def presenceAtRemoteSite(model, day, week, shift, required_site, required_number):
def presenceAtRemoteSite(
model, day, week, shift, required_site, required_number
):
required_site_workers = [
w for w in self.workers if required_site == w.remote_site
]
@@ -836,10 +866,7 @@ class RotaBuilder(object):
return Constraint.Skip
return (
sum(
model.works[w.id, week, day, shift]
for w in required_site_workers
)
sum(model.works[w.id, week, day, shift] for w in required_site_workers)
>= required_number
)
@@ -886,7 +913,7 @@ class RotaBuilder(object):
# )
for site in self.sites:
for shift in self.get_shifts_with_constraint("balance_across_groups"):
if site in shift.site:
if site in shift.sites:
block = shift.name
for week in self.weeks:
self.model.constraints.add(
@@ -919,7 +946,7 @@ class RotaBuilder(object):
]
>= sum(
self.model.works[worker.id, week, day, shift_name]
for day in shift.shift_days
for day in shift.days
)
)
self.model.constraints.add(
@@ -941,8 +968,12 @@ class RotaBuilder(object):
for w in workers:
if w.non_working_day_list:
l = []
for nwd, start_nwd_date, end_nwd_date in w.non_working_day_list:
if nwd in shift.shift_days:
for (
nwd,
start_nwd_date,
end_nwd_date,
) in w.non_working_day_list:
if nwd in shift.days:
if start_nwd_date > self.get_week_start_date(week):
continue
if end_nwd_date < self.get_week_start_date(week):
@@ -957,8 +988,6 @@ class RotaBuilder(object):
else:
full_workers.append(w)
print(nwd_workers)
if not nwd_workers: # Just do the usual
self.model.constraints.add(
sum(
@@ -1015,6 +1044,33 @@ class RotaBuilder(object):
)
)
for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]:
if invalid_shifts := set(constraint_dict["shifts"]).difference(
set(self.get_shift_names())
):
raise ValueError(
f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}"
)
if invalid_grades := set(constraint_dict["grades"]).difference(
set(self.get_worker_grades())
):
raise ValueError(
f"avoid shifts by grade constraint contains a non existent worker grade: {invalid_grades}"
)
if worker.grade in constraint_dict["grades"]:
self.model.constraints.add(
0
== sum(
self.model.works[worker.id, week, day, shift]
for week, day, shift in self.get_all_shiftname_combinations()
if week in constraint_dict["weeks"]
and shift in constraint_dict["shifts"]
)
)
# sys.exit(0)
if self.constraint_options["avoid_st2_first_month"] and worker.grade == 2:
# Avoid ST1s on the first month
try:
@@ -1049,15 +1105,17 @@ class RotaBuilder(object):
# no solution will be possible
for shift in self.get_shifts():
if (
worker.site in shift.site
worker.site in shift.sites
): # Each site specfies which sites self.workers can fullfill it
# calaculate the total number of shifts that need assigning over the rota
# total_shifts = (len(self.weeks) * len(shift.shift_days) *
total_shifts = self.shift_counts[shift] * shift.workers_required
# total_shifts = (len(self.weeks) * len(shift.days) *
total_shifts = (
self.shift_counts[shift.name] * shift.workers_required
)
full_time_equivalent_joined = sum(
self.full_time_equivalent_sites[i] for i in shift.site
self.full_time_equivalent_sites[i] for i in shift.sites
)
target_shifts = (
@@ -1111,16 +1169,18 @@ class RotaBuilder(object):
)
else:
# Make sure shifts aren't assigned to those from other sites
self.model.constraints.add(
0
== sum(
self.model.works[worker.id, week, day, shift.name]
for week, day in self.get_week_day_combinations_for_shift(
shift
# Not needed if there are no shifts
if self.get_week_day_combinations_for_shift(shift):
self.model.constraints.add(
0
== sum(
self.model.works[worker.id, week, day, shift.name]
for week, day in self.get_week_day_combinations_for_shift(
shift
)
# for week, day in self.get_week_day_combinations()
)
# for week, day in self.get_week_day_combinations()
)
)
# model.shift_count stores the number of shifts that a worker is assigned to worker by shift
# this is used (by the objective) to balance the total number of shifts worked
@@ -1147,7 +1207,7 @@ class RotaBuilder(object):
if self.constraint_options["balance_shifts_quadratic"]:
# This may need to be updated
xU = 10
xU = 25
xL = 1
self.model.constraints.add(
inequality(
@@ -1304,9 +1364,9 @@ class RotaBuilder(object):
# We use a similar method to balance the number of weekends worked
# This works as long as weekend shifts are assigned as blocks!
weekend_shift_target_number = sum(
worker.shift_target_number[shift.name] / len(shift.shift_days)
worker.shift_target_number[shift.name] / len(shift.days)
for shift in self.get_shifts()
if "Sat" in shift.shift_days or "Sun" in shift.shift_days
if "Sat" in shift.days or "Sun" in shift.days
)
worker.weekend_shift_target_number = weekend_shift_target_number
@@ -1374,7 +1434,7 @@ class RotaBuilder(object):
if self.constraint_options["balance_blocks"]:
for week_blocks in self.get_week_block_iterator(
self.max_night_frequency
self.constraint_options["max_night_frequency"]
):
if self.get_shifts_with_constraint("night"):
# Prevent nights more than once every n weeks
@@ -1390,7 +1450,7 @@ class RotaBuilder(object):
)
# if self.constraint_options["balance_weekends"]:
for week_blocks in self.get_week_block_iterator(self.max_weekend_frequency):
for week_blocks in self.get_week_block_iterator(self.constraint_options["max_weekend_frequency"]):
# Prevent weekend shifts more than once every n weeks
self.model.constraints.add(
1
@@ -1400,12 +1460,55 @@ class RotaBuilder(object):
)
)
for week in self.weeks:
for constraint_shift in self.get_shifts_with_constraints(
"max_shifts_per_week_block"
):
constraint_options = constraint_shift.constraint_options[
"max_shifts_per_week_block"
]
# If not supplied, default to the number of days per shift
shift_number = constraint_shift.get_shift_number()
if "shift_number" in constraint_options:
shift_number = constraint_options["shift_number"]
for week_blocks in self.get_week_block_iterator(
constraint_options["weeks"]
):
# Prevent shifts more than once every n weeks
self.model.constraints.add(
shift_number
>= sum(
self.model.works[
worker.id, week, day, constraint_shift.name
]
for week in week_blocks
for day in constraint_shift.days
)
)
for week in self.weeks:
for constraint_shift in self.get_shifts_with_constraints(
"max_shifts_per_week"
):
self.model.constraints.add(
constraint_shift.constraint_options["max_shifts_per_week"]
>= sum(
self.model.works[worker.id, w, day, constraint_shift.name]
# for shiftname in self.get_shift_names_by_week_day(week, day)
# for day in self.days
for w, day in self.get_week_day_combinations_for_shift(
constraint_shift
)
if w == week
)
)
self.model.constraints.add(
self.constraint_options["max_shifts_per_week"]
>= sum(
self.model.works[worker.id, week, day, shiftname]
self.model.works[worker.id, w, day, shiftname]
# for shiftname in self.get_shift_names_by_week_day(week, day)
# for day in self.days
for w, day, shiftname in self.get_all_shiftname_combinations(
@@ -1438,7 +1541,7 @@ class RotaBuilder(object):
+ sum(
self.model.works[worker.id, week + 1, "Mon", shift]
for shift in self.get_shift_names_by_week_day(
week+1, "Mon"
week + 1, "Mon"
)
)
)
@@ -1485,21 +1588,15 @@ class RotaBuilder(object):
full_weekend_count
>= sum(
self.model.works[worker.id, week, "Sat", shift.name]
for shift in self.get_shift_names_by_week_day(
week, "Sat"
)
for shift in self.get_shift_names_by_week_day(week, "Sat")
)
+ sum(
self.model.works[worker.id, week, "Sun", shift.name]
for shift in self.get_shift_names_by_week_day(
week, "Sat"
)
for shift in self.get_shift_names_by_week_day(week, "Sat")
)
+ sum(
self.model.works[worker.id, week, "Fri", shift.name]
for shift in self.get_shift_names_by_week_day(
week, "Sat"
)
for shift in self.get_shift_names_by_week_day(week, "Sat")
)
)
if (
@@ -1510,21 +1607,15 @@ class RotaBuilder(object):
full_weekend_count
>= sum(
self.model.works[worker.id, week, "Sat", shift.name]
for shift in self.get_shift_names_by_week_day(
week, "Sat"
)
for shift in self.get_shift_names_by_week_day(week, "Sat")
)
+ sum(
self.model.works[worker.id, week, "Sun", shift.name]
for shift in self.get_shift_names_by_week_day(
week, "Sun"
)
for shift in self.get_shift_names_by_week_day(week, "Sun")
)
+ sum(
self.model.works[worker.id, week, "Thu", shift.name]
for shift in self.get_shift_names_by_week_day(
week, "Thu"
)
for shift in self.get_shift_names_by_week_day(week, "Thu")
)
)
@@ -1576,6 +1667,27 @@ class RotaBuilder(object):
# self.model.shift_week_worker_assigned[s.name, week, worker.id]
# for s in self.get_shifts_with_constraint("night")))
#for shift in self.get_shifts_with_constraint("single_block_per_week"):
# if shift.force_as_block:
# # Force nights to be assigned in blocks
# # self.model.constraints.add(8* self.model.shift_week_worker_assigned[shift.name, week, worker.id] >= sum(self.model.works[worker.id, week, day, shift.name] for day in self.days)
# # )
# # Force night shifts to be assigned in blocks
# self.model.constraints.add(
# self.model.shift_week_worker_assigned[
# shift.name, week, worker.id
# ]
# * len(shift.days)
# == sum(
# self.model.works[worker.id, w, d, s]
# for w, d, s in self.get_all_shiftname_combinations(week=week)
# #for day in self.days
# )
# )
# else:
# raise ValueError(f"Requires {shift.name} to have force_as_block")
# for shift in self.get_shifts_with_constraint("night"):
for shift in self.get_shifts():
if shift.force_as_block:
@@ -1588,10 +1700,10 @@ class RotaBuilder(object):
self.model.shift_week_worker_assigned[
shift.name, week, worker.id
]
* len(shift.shift_days)
* len(shift.days)
== sum(
self.model.works[worker.id, week, day, shift.name]
for day in shift.shift_days
for day in shift.days
)
)
@@ -1603,6 +1715,9 @@ class RotaBuilder(object):
p1 = 1
p2 = 1
p3 = 1
p4 = 1
p5 = 1
if n > 0:
pweek, pday = weeks_days[n - 1]
else:
@@ -1615,6 +1730,24 @@ class RotaBuilder(object):
p2week, p2day = weeks_days[n]
p2 = 0
try:
p3week, p3day = weeks_days[n - 3]
except IndexError:
p3week, p3day = weeks_days[n]
p3 = 0
try:
p4week, p4day = weeks_days[n - 4]
except IndexError:
p4week, p4day = weeks_days[n]
p4 = 0
try:
p5week, p5day = weeks_days[n - 5]
except IndexError:
p5week, p5day = weeks_days[n]
p5 = 0
n1 = 1
n2 = 1
try:
@@ -1679,7 +1812,7 @@ class RotaBuilder(object):
for constraint_shift in self.get_shifts_with_constraints(
"preclear", "preclear2"
):
if day in constraint_shift.shift_days:
if day in constraint_shift.days:
self.model.constraints.add(
1
>= self.model.works[
@@ -1696,7 +1829,7 @@ class RotaBuilder(object):
)
for constraint_shift in self.get_shifts_with_constraint("preclear2"):
if day in constraint_shift.shift_days:
if day in constraint_shift.days:
self.model.constraints.add(
1
>= self.model.works[
@@ -1715,7 +1848,7 @@ class RotaBuilder(object):
for constraint_shift in self.get_shifts_with_constraints(
"postclear", "postclear2"
):
if day in constraint_shift.shift_days:
if day in constraint_shift.days:
self.model.constraints.add(
1
>= self.model.works[
@@ -1732,7 +1865,7 @@ class RotaBuilder(object):
)
for constraint_shift in self.get_shifts_with_constraints("postclear2"):
if day in constraint_shift.shift_days:
if day in constraint_shift.days:
self.model.constraints.add(
1
>= self.model.works[
@@ -1960,14 +2093,13 @@ class RotaBuilder(object):
"""
for worker in workers:
self.add_worker(worker)
#self.workers.extend(workers)
# self.workers.extend(workers)
def build_workers(self) -> None:
"""Process loaded shifts and workers
Must be called prior to attempting to solve
"""
# self.build_shifts()
if not self.workers:
raise NoWorkers("Workers must be added prior to calling build_workers")
@@ -1982,11 +2114,11 @@ class RotaBuilder(object):
self.workers_id_map[wid] = worker
if worker.name in self.workers_name_map:
raise ValueError(f"Worker with name '{worker.name}' has been added twice")
raise ValueError(
f"Worker with name '{worker.name}' has been added twice"
)
self.workers_name_map[worker.name] = worker
self.workers = sorted(self.workers)
self.full_time_equivalent = sum(w.fte_adj for w in self.workers)
@@ -2007,7 +2139,14 @@ class RotaBuilder(object):
for p in pairs:
self.worker_pairs.append(tuple(pairs[p]))
def add_shift(self, shift) -> None:
def add_grade_constraint_by_week(
self, grades: Iterable[int], weeks: Iterable[int], shifts
):
self.constraint_options["avoid_shifts_by_grades"].append(
{"grades": grades, "weeks": weeks, "shifts": shifts}
)
def add_shift(self, shift: SingleShift) -> None:
"""Add a shift to the collection
:param SingleShift shift: Shift object
@@ -2023,7 +2162,7 @@ class RotaBuilder(object):
"""
self.shifts.extend(shifts)
def get_shift_names_by_week_day(self, week, day: DayStr) -> set():
def get_shift_names_by_week_day(self, week, day: DayStr) -> set:
"""Returns the shifts required for a specific day
Returns:
@@ -2066,6 +2205,9 @@ class RotaBuilder(object):
self.week_day_shift_product = []
self.week_day_shiftclass_product = []
for s in self.shifts:
pass
for s in self.shifts:
if s.name in self.shift_names:
raise InvalidShift(f"Duplicate shift: {s.name}")
@@ -2073,10 +2215,10 @@ class RotaBuilder(object):
self.shifts_by_name[s.name] = s
self.shift_names.append(s.name)
# for day in s.shift_days:
# for day in s.days:
# self.week_day_shifts_dict[(week, day)].add(s.name)
for site in s.site:
for site in list(s.sites):
self.sites.add(site)
self.shift_counts = defaultdict(int)
@@ -2085,16 +2227,17 @@ class RotaBuilder(object):
for s in self.shifts:
if s.bank_holidays_only:
if self.week_day_date_map[(week, day)] in bank_holiday_map:
print(s.name, self.week_day_date_map[(week, day)])
self.week_day_shift_product.append((week, day, s.name))
self.week_day_shiftclass_product.append((week, day, s))
self.week_day_shifts_dict[(week, day)].add(s.name)
self.shift_counts[s] = self.shift_counts[s] + 1
self.shift_counts[s.name] = self.shift_counts[s.name] + 1
else:
if day in s.shift_days:
if day in s.days:
self.week_day_shift_product.append((week, day, s.name))
self.week_day_shiftclass_product.append((week, day, s))
self.week_day_shifts_dict[(week, day)].add(s.name)
self.shift_counts[s] = self.shift_counts[s] + 1
self.shift_counts[s.name] = self.shift_counts[s.name] + 1
# print(self.week_day_shift_product)
# # Todo replace with week_day.....
@@ -2102,7 +2245,7 @@ class RotaBuilder(object):
# self.day_shiftclass_product = []
# for s in self.shifts:
# for day in days:
# if day in s.shift_days:
# if day in s.days:
# self.day_shift_product.append((day, s.name))
# self.day_shiftclass_product.append((day, s))
#
@@ -2193,17 +2336,27 @@ class RotaBuilder(object):
"""
return self.shifts
def get_shifts_for_worker_site(self, worker_site):
shifts = [shift for shift in self.shifts if worker_site in shift.sites]
return shifts
def get_shifts_for_worker(self, worker_id):
worker = self.get_worker_by_id(worker_id)
shifts = [shift for shift in self.shifts if worker.site in shift.sites]
return shifts
def get_shifts_with_constraint(self, constraint) -> List[SingleShift]:
return [shift for shift in self.shifts if constraint in shift.constraints]
def get_shifts_with_constraints(self, *constraints) -> set[SingleShift]:
shifts = set()
def get_shifts_with_constraints(self, *constraints) -> List[SingleShift]:
shift_names = set()
for constraint in constraints:
shifts.update(
[shift for shift in self.shifts if constraint in shift.constraints]
shift_names.update(
[shift.name for shift in self.shifts if constraint in shift.constraints]
)
return shifts
return [self.get_shift_by_name(s) for s in shift_names]
def get_shift_names(self) -> List[ShiftName]:
"""Returns a list of all the registered shift names
@@ -2239,12 +2392,12 @@ class RotaBuilder(object):
"""Returns a list of all possible shifts, the workers required and site
Returns:
list: list (week, day, shift name, workers required, shift site)
list: list (week, day, shift name, workers required, shift.sites)
"""
l = []
for week, day, shift in self.week_day_shiftclass_product:
# if day in shift.shift_days:
l.append((week, day, shift.name, shift.workers_required, shift.site))
# if day in shift.days:
l.append((week, day, shift.name, shift.workers_required, shift.sites))
return l
@@ -2265,17 +2418,24 @@ class RotaBuilder(object):
return l - set(self.get_all_shiftclass_combinations())
def shifts_to_assign_as_blocks(self):
def shifts_to_assign_as_blocks(self) -> List[str]:
return [shift.name for shift in self.get_shifts() if shift.assign_as_block]
def shifts_to_force_as_blocks(self):
return [shift.name for shift in self.get_shifts() if (shift.force_as_block or shift.force_as_block_unless_nwd)]
def shifts_to_force_as_blocks(self) -> List[str]:
return [
shift.name
for shift in self.get_shifts()
if (shift.force_as_block or shift.force_as_block_unless_nwd)
]
def shifts_to_assign_or_force_as_blocks(self):
def shifts_to_assign_or_force_as_blocks(self) -> List[str]:
s = self.shifts_to_assign_as_blocks()
s.extend(self.shifts_to_force_as_blocks())
return s
def get_worker_by_id(self, worker_id: str) -> Worker:
return self.workers_id_map[worker_id]
def get_worker_by_name(self, name: str) -> Worker:
return self.workers_name_map[name]
@@ -2301,7 +2461,7 @@ class RotaBuilder(object):
return group_workers
def get_workers_for_shift(self, shift: SingleShift) -> List[Worker]:
return [worker for worker in self.workers if worker.site in shift.site]
return [worker for worker in self.workers if worker.site in shift.sites]
def get_workers_total_fte(self) -> float:
return self.full_time_equivalent
@@ -2322,6 +2482,9 @@ class RotaBuilder(object):
def get_workers(self):
return self.workers
def get_worker_grades(self) -> set[int]:
return set([worker.grade for worker in self.workers])
def get_bank_holiday_week_days(self):
return [
(week, day)
@@ -2385,7 +2548,6 @@ class RotaBuilder(object):
week_table[week][day][shift].append(worker.get_details())
return week_table
def get_worker_timetable(self):
works = self.model.works
timetable = {
@@ -2481,7 +2643,9 @@ class RotaBuilder(object):
if d in bank_holiday_map:
th_class = "bank-holiday"
date_row.append(f"<th title='{d}' class='{th_class}' data-date='{d}'>Week {week}: {day}</th>")
date_row.append(
f"<th title='{d}' class='{th_class}' data-date='{d}'>Week {week}: {day}</th>"
)
n = n + 1
timetable.append(f"<tr class='data-row'>{''.join(date_row)}</tr>")
@@ -2499,10 +2663,10 @@ class RotaBuilder(object):
shifts = []
nwds = json.dumps(worker.non_working_day_list, default=str)
#if worker.nwd:
# if worker.nwd:
# # TODO: limit to dates
# nwds = ", ".join([i[0] for i in worker.nwd])
#else:
# else:
# nwds = None
worker_targets = json.dumps(worker.shift_target_number)
@@ -2667,7 +2831,7 @@ class RotaBuilder(object):
html = f"""
<body>
<div class="table-div" id="{table_name}">
<table>{joined_timetable}</table>
<table id="main-table">{joined_timetable}</table>
</div>
<details>
<summary><h2>Rota settings</h2></summary>
@@ -2688,7 +2852,14 @@ class RotaBuilder(object):
<pre>
{result_string}
</pre>
</details>
<div>
<details>
<summary><h2>Export table</h2></summary>
<div id="export-div">
<div>
</details>
</div
</body>
"""
@@ -2730,7 +2901,7 @@ class RotaBuilder(object):
temp = ""
for shift in self.get_shift_names_by_week_day(week, day):
if self.model.works[worker.id, week, day, shift].value > 0:
shifts[d]=shift
shifts[d] = shift
return shifts
@@ -2738,12 +2909,12 @@ class RotaBuilder(object):
shifts = {}
for worker in self.workers:
shifts[worker.name] = len([i for i in self.get_worker_shift_list(worker) if i != ""])
shifts[worker.name] = len(
[i for i in self.get_worker_shift_list(worker) if i != ""]
)
return shifts
def get_worker_shift_list(self, worker: Worker) -> List:
shifts = []
@@ -2822,14 +2993,17 @@ class RotaBuilder(object):
class NoActiveSites(Exception):
"""Raised when there are no active sites"""
pass
class NoWorkers(Exception):
"""Raised when there are no active sites"""
pass
class InvalidShift(Exception):
"""Raised when there are no active sites"""
pass
pass