start pydanitification

This commit is contained in:
Ross
2022-05-26 15:38:35 +01:00
parent a8cf3e3460
commit c9ba2a098c
14 changed files with 767 additions and 459 deletions
+12 -2
View File
@@ -23,6 +23,9 @@ function generateExtra() {
$(".auto-generated").remove(); $(".auto-generated").remove();
global_shifts = new Set(); global_shifts = new Set();
total_summed_shift_diffs = 0;
total_summed_shift_diffs_abs = 0;
tables.each((n, table) => { tables.each((n, table) => {
rows = $(table).find(".worker-row"); rows = $(table).find(".worker-row");
@@ -114,7 +117,7 @@ function generateExtra() {
(n, td_el) => { (n, td_el) => {
td_date = new Date(Date.parse(td_el.dataset.date)); td_date = new Date(Date.parse(td_el.dataset.date));
if (td_date >= start_date && td_date <= end_date) { if (td_date >= start_date && td_date < end_date) {
$(td_el).addClass("nwd"); $(td_el).addClass("nwd");
} }
@@ -150,6 +153,9 @@ function generateExtra() {
shift_diff = JSON.parse(worker_td.attr("data-shift-diff")); shift_diff = JSON.parse(worker_td.attr("data-shift-diff"));
summed_shift_diff = Object.values(shift_diff).reduce((a, b) => a + b); summed_shift_diff = Object.values(shift_diff).reduce((a, b) => a + b);
total_summed_shift_diffs = total_summed_shift_diffs + summed_shift_diff;
total_summed_shift_diffs_abs = total_summed_shift_diffs_abs + Math.abs(summed_shift_diff);
worker_td.after(`<div class='worker-summary auto-generated'> worker_td.after(`<div class='worker-summary auto-generated'>
<span>Total shifts: ${total_shifts}, </span> <span>Total shifts: ${total_shifts}, </span>
@@ -166,6 +172,7 @@ function generateExtra() {
}) })
sites = {}; sites = {};
sites_days_lost = {} sites_days_lost = {}
for (const worker in workers) { for (const worker in workers) {
@@ -216,6 +223,8 @@ function generateExtra() {
}); });
$(table).before($(`<span>Total summed shift diff: ${total_summed_shift_diffs}</span><br/>`));
$(table).before($(`<span>Total summed shift diff: ${total_summed_shift_diffs_abs}</span><br/>`));
$(table).before(summary_button); $(table).before(summary_button);
@@ -363,7 +372,8 @@ function viewWorker(worker) {
FTE: ${ds.fte} (${ds.fte_adj})</br> FTE: ${ds.fte} (${ds.fte_adj})</br>
Start date: ${ds.start_date}</br> Start date: ${ds.start_date}</br>
End date: ${ds.end_date}</br> End date: ${ds.end_date}</br>
Non working days: ${ds.nwds} Non working days: ${ds.nwds}</br>
Remote site: ${ds.remoteSite}
</div>`) </div>`)
$("body").append(dlg) $("body").append(dlg)
+3 -1
View File
@@ -1,4 +1,6 @@
pyomo pyomo
govuk_bank_holidays govuk_bank_holidays
pytest pytest
black black
rich
pydantic
+1 -1
View File
@@ -227,7 +227,7 @@ if use_test_workers:
] ]
) )
Rota.add_workers( Rota.add_workers(
[Worker(Rota, i, "Plym {}".format(i), "plymouth", 4, 50) for i in range(9, 17)] [Worker(i, "Plym {}".format(i), "plymouth", 4, 50) for i in range(9, 17)]
) )
Rota.add_workers( Rota.add_workers(
[ [
+74 -23
View File
@@ -40,7 +40,7 @@ for bank_holiday in bank_holidays.get_holidays(division="england-and-wales"):
SHIFT_BOUNDS = { SHIFT_BOUNDS = {
"bank_holiday": (0, 9), "bank_holiday": (0, 9),
"shift_count": (0, 400), "shift_count": (0, 400),
"night_shift_count": (0, 19), "night_shift_count": (0, 100),
"weekend_count": (0, 60), "weekend_count": (0, 60),
} }
@@ -55,7 +55,7 @@ class SingleShift(object):
postclear(2) / preclear(2) postclear(2) / preclear(2)
require_remote_site_presence: require_remote_site_presence_week:
options: (site, required_number) options: (site, required_number)
night night
@@ -185,6 +185,7 @@ class RotaBuilder(object):
self.unavailable_to_work = set() self.unavailable_to_work = set()
self.unavailable_to_work_reason = {} self.unavailable_to_work_reason = {}
self.pref_not_to_work = {} self.pref_not_to_work = {}
self.pref_not_to_work_reason = {}
self.work_requests = set() self.work_requests = set()
self.work_requests_map = {} self.work_requests_map = {}
@@ -203,14 +204,13 @@ class RotaBuilder(object):
self.max_weekend_frequency = max_weekend_frequency self.max_weekend_frequency = max_weekend_frequency
self.constraint_options = { self.constraint_options = {
"ensure_1_st4_plus_on_nights": True,
"balance_nights": True, "balance_nights": True,
"constrain_time_off_after_nights": False, "constrain_time_off_after_nights": False,
"balance_nights_across_sites": True, "balance_nights_across_sites": True,
"balance_bank_holidays": True, "balance_bank_holidays": True,
"balance_blocks": True, "balance_blocks": True,
"balance_shifts": True, # Does not use a quadratic function "balance_shifts": True, # Does not use a quadratic function
"balance_shifts_quadratic": False, "balance_shifts_quadratic": False, # Will prevent spreading of spreading across different shifts
"balance_shifts_over_workers": True, "balance_shifts_over_workers": True,
"minimise_shift_diffs": False, # less sophisticated version of balance_shifts_over_workers "minimise_shift_diffs": False, # less sophisticated version of balance_shifts_over_workers
"balance_weekends": True, "balance_weekends": True,
@@ -223,8 +223,6 @@ class RotaBuilder(object):
"prevent_monday_and_tuesday_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": [], "prevent_thursdays_before_full_weekends": [],
# This may be better defined on the shift?
"require_presence_at_site_overnight": [],
"avoid_st2_first_month": False, "avoid_st2_first_month": False,
"hard_constrain_pair_separation": False, "hard_constrain_pair_separation": False,
} }
@@ -804,7 +802,7 @@ class RotaBuilder(object):
), ),
) )
def presenceAtRemoteSite(model, week, shift, required_site, required_number): def presenceAtRemoteSiteWeek(model, week, shift, required_site, required_number):
required_site_workers = [ required_site_workers = [
w for w in self.workers if required_site == w.remote_site w for w in self.workers if required_site == w.remote_site
] ]
@@ -817,6 +815,40 @@ class RotaBuilder(object):
>= required_number >= required_number
) )
for shift in self.get_shifts_with_constraint("require_remote_site_presence_week"):
site, required_number = shift.constraint_options[
"require_remote_site_presence_week"
]
# self.model.require_presence_at_site_overnight_rule = Constraint(
setattr(
self.model,
f"require_remote_site_presence_week_{shift.name}",
Constraint(
[week for week in self.weeks],
[shift.name],
[site],
[required_number],
rule=presenceAtRemoteSiteWeek,
),
)
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
]
for w in required_site_workers:
if (w.id, week, day, shift) not in model.works:
return Constraint.Skip
return (
sum(
model.works[w.id, week, day, shift]
for w in required_site_workers
)
>= required_number
)
for shift in self.get_shifts_with_constraint("require_remote_site_presence"): for shift in self.get_shifts_with_constraint("require_remote_site_presence"):
site, required_number = shift.constraint_options[ site, required_number = shift.constraint_options[
"require_remote_site_presence" "require_remote_site_presence"
@@ -826,6 +858,7 @@ class RotaBuilder(object):
self.model, self.model,
f"require_remote_site_presence_{shift.name}", f"require_remote_site_presence_{shift.name}",
Constraint( Constraint(
[day for day in self.days],
[week for week in self.weeks], [week for week in self.weeks],
[shift.name], [shift.name],
[site], [site],
@@ -912,10 +945,9 @@ class RotaBuilder(object):
full_workers = [] full_workers = []
for w in workers: for w in workers:
if w.nwd: if w.non_working_day_list:
l = [] l = []
# This should take into account dates! for nwd, start_nwd_date, end_nwd_date in w.non_working_day_list:
for nwd, start_nwd_date, end_nwd_date in w.nwd:
if nwd in shift.shift_days: if nwd in shift.shift_days:
if start_nwd_date > self.get_week_start_date(week): if start_nwd_date > self.get_week_start_date(week):
continue continue
@@ -1121,7 +1153,7 @@ class RotaBuilder(object):
if self.constraint_options["balance_shifts_quadratic"]: if self.constraint_options["balance_shifts_quadratic"]:
# This may need to be updated # This may need to be updated
xU = 25 xU = 10
xL = 1 xL = 1
self.model.constraints.add( self.model.constraints.add(
inequality( inequality(
@@ -1333,9 +1365,9 @@ class RotaBuilder(object):
) )
# Ensure worker is not allocated shifts on non working days # Ensure worker is not allocated shifts on non working days
if worker.nwd: if worker.non_working_day_list:
for week, day, shift in self.get_all_shiftclass_combinations(): for week, day, shift in self.get_all_shiftclass_combinations():
for n, start, end in worker.nwd: for n, start, end in worker.non_working_day_list:
if not shift.rota_on_nwds and day == n: if not shift.rota_on_nwds and day == n:
# print(start, self.week_day_date_map[(week, day)], end) # print(start, self.week_day_date_map[(week, day)], end)
if start <= self.week_day_date_map[(week, day)] < end: if start <= self.week_day_date_map[(week, day)] < end:
@@ -1932,7 +1964,9 @@ class RotaBuilder(object):
Args: Args:
workers (List(Worker)): workers (List(Worker)):
""" """
self.workers.extend(workers) for worker in workers:
self.add_worker(worker)
#self.workers.extend(workers)
def build_workers(self) -> None: def build_workers(self) -> None:
"""Process loaded shifts and workers """Process loaded shifts and workers
@@ -1947,6 +1981,7 @@ class RotaBuilder(object):
self.workers_name_map = {} self.workers_name_map = {}
for worker in self.workers: for worker in self.workers:
worker.load_rota(self)
wid = worker.id wid = worker.id
if wid in self.workers_id_map: if wid in self.workers_id_map:
raise ValueError(f"Worker with id '{wid}' has been added twice") raise ValueError(f"Worker with id '{wid}' has been added twice")
@@ -2251,16 +2286,23 @@ class RotaBuilder(object):
return self.workers_name_map[name] return self.workers_name_map[name]
def get_workers_by_group(self) -> Dict[str, Worker]: def get_workers_by_group(self) -> Dict[str, Worker]:
group_workers = defaultdict(set) group_workers = defaultdict(list)
for worker in self.workers: for worker in self.workers:
group_workers[worker.site].add(worker) group_workers[worker.site].append(worker)
return group_workers
def get_workers_by_remote_group(self) -> Dict[str, Worker]:
group_workers = defaultdict(list)
for worker in self.workers:
group_workers[worker.remote_site].append(worker)
return group_workers return group_workers
def get_workers_by_grade(self) -> Dict[str, Worker]: def get_workers_by_grade(self) -> Dict[str, Worker]:
group_workers = defaultdict(set) group_workers = defaultdict(list)
for worker in self.workers: for worker in self.workers:
group_workers[worker.grade].add(worker) group_workers[worker.grade].append(worker)
return group_workers return group_workers
@@ -2462,7 +2504,7 @@ class RotaBuilder(object):
current_site = worker.site current_site = worker.site
shifts = [] shifts = []
nwds = json.dumps(worker.nwd, default=str) nwds = json.dumps(worker.non_working_day_list, default=str)
#if worker.nwd: #if worker.nwd:
# # TODO: limit to dates # # TODO: limit to dates
# nwds = ", ".join([i[0] for i in worker.nwd]) # nwds = ", ".join([i[0] for i in worker.nwd])
@@ -2525,8 +2567,8 @@ class RotaBuilder(object):
remote_site = "" remote_site = ""
if shift_name: if shift_name:
shift = self.get_shift_by_name(shift_name) shift = self.get_shift_by_name(shift_name)
if "require_remote_site_presence" in shift.constraints: if "require_remote_site_presence_week" in shift.constraints:
remote_site = f" data-shift-remote-site='{shift.constraint_options['require_remote_site_presence'][0]}'" remote_site = f" data-shift-remote-site='{shift.constraint_options['require_remote_site_presence_week'][0]}'"
shift_tds.append( shift_tds.append(
f"<td title='{title}' class='rota-day {css_class}' data-shift='{shift_name}' data-available='{available}' data-unavailable_reason='{unavailable_reason}' data-date='{d}' data-week='{week}' data-day='{day}'{remote_site}{requests}{bank_holiday}>{a}</td>" f"<td title='{title}' class='rota-day {css_class}' data-shift='{shift_name}' data-available='{available}' data-unavailable_reason='{unavailable_reason}' data-date='{d}' data-week='{week}' data-day='{day}'{remote_site}{requests}{bank_holiday}>{a}</td>"
@@ -2564,8 +2606,8 @@ class RotaBuilder(object):
name=worker.name, name=worker.name,
fte=worker.fte, fte=worker.fte,
fte_adj=worker.fte_adj, fte_adj=worker.fte_adj,
start_date=worker.start_date, start_date=worker.calculated_start_date,
end_date=worker.end_date, end_date=worker.calculated_end_date,
targets=worker_targets, targets=worker_targets,
worker_shift_counts=json.dumps(shift_count_dict), worker_shift_counts=json.dumps(shift_count_dict),
weekend_target=worker.weekend_shift_target_number, weekend_target=worker.weekend_shift_target_number,
@@ -2698,6 +2740,15 @@ class RotaBuilder(object):
return shifts return shifts
def get_workers_total_shifts(self) -> Dict[str, int]:
shifts = {}
for worker in self.workers:
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: def get_worker_shift_list(self, worker: Worker) -> List:
shifts = [] shifts = []
+176 -139
View File
@@ -1,6 +1,9 @@
import datetime import datetime
from collections import defaultdict from collections import defaultdict
from typing import Iterable, List from typing import Iterable, List, Literal, Optional
from pydantic import BaseModel, Extra
from rich.pretty import pprint
# from .shifts import RotaBuilder, days, sites # from .shifts import RotaBuilder, days, sites
import uuid import uuid
@@ -11,175 +14,204 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from rota.shifts import RotaBuilder from rota.shifts import RotaBuilder
days = Literal["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
class Worker: class NotAvailableToWork(BaseModel):
def __init__( date: datetime.date
self, reason: str = "unknown"
Rota: "RotaBuilder", # Aim to remove (workers should be independent of rotas)
name: str,
site: str,
grade: int,
id=None,
fte: int = 100,
nwd: list[str] | list[str, datetime.datetime, datetime.datetime] = [],
start_date: datetime.datetime | None = None,
end_date: datetime.datetime | None = None,
oop: list[(datetime.datetime, datetime.datetime, str)] | None = None,
not_available_to_work: list[(datetime.datetime, str)] | None = None,
pref_not_to_work: list[datetime.datetime] | None = None,
work_requests: list[(datetime.datetime, str)] | None = None,
remote_site: str = "plymouth", # We set a default proc_site
previous_shifts: dict = {},
shift_balance_extra: dict = {},
bank_holiday_extra: int = 0,
pair: int = 0,
):
# We can either have a user generated ID class NonWorkingDays(BaseModel):
if id is not None: day: days
self.id = id start_date: datetime.date | None = None
else: end_date: datetime.date | None = None
self.id = uuid.uuid4()
self.name = name class WorkRequests(BaseModel):
self.site = site date: datetime.date
# Grade are equivalent to roles, by keeping them integer defining model shift: str = ""
# rules is easier.
self.grade = grade class PreferenceNotToWork(BaseModel):
self.fte = fte date: datetime.date
self.nwd = [] reason: str = ""
class OutOfProgramme(BaseModel):
start_date: datetime.date
end_date: datetime.date
reason: str = ""
class Worker(BaseModel):
name: str
site: str
# Grade are equivalent to roles, by keeping them integer defining model
# rules is easier.
grade: int
# We can either have a user generated ID
id: Optional[int]=None
fte: int = 100
nwds: list[NonWorkingDays] = []
start_date: datetime.date | None = None
end_date: datetime.date | None = None
oop: list[OutOfProgramme] = []
not_available_to_work: list[NotAvailableToWork] = []
pref_not_to_work: list[PreferenceNotToWork] = []
work_requests: list[WorkRequests] = []
remote_site: str = "plymouth", # We set a default proc_site
previous_shifts: dict = {}
shift_balance_extra: dict = {}
bank_holiday_extra: int = 0
pair: int = 0
class Config:
extra = Extra.allow
# def __init__(
# self,
# #Rota: "RotaBuilder", # Aim to remove (workers should be independent of rotas)
# ):
#
# self.name = name
# self.site = site
# self.grade = grade
# self.fte = fte
# self.nwd = nwd
# self.pair = pair
# self.shift_balance_extra = shift_balance_extra
# self.bank_holiday_extra = bank_holiday_extra
# self.start_date
#
# self.remote_site = remote_site
#
# self.previous_shifts = previous_shifts
#
#
# # days_to_work = Rota.rota_days_length
def load_rota(self, Rota):
self.proportion_rota_to_work = 1 self.proportion_rota_to_work = 1
self.pair = pair
self.shift_balance_extra = shift_balance_extra
self.bank_holiday_extra = bank_holiday_extra
self.remote_site = remote_site
self.previous_shifts = previous_shifts
self.shift_target_number = defaultdict(int) self.shift_target_number = defaultdict(int)
# days_to_work = Rota.rota_days_length if self.id is None:
self.id = uuid.uuid4()
if start_date is not None and end_date is not None: if self.start_date is not None and self.end_date is not None:
if start_date >= end_date: if self.start_date >= self.end_date:
raise ValueError("End date must be after start date") raise ValueError("End date must be after start date")
# if no start date default to the start of the rota # if no start date default to the start of the rota
if start_date is None: if self.start_date is None:
self.start_date = Rota.start_date self.calculated_start_date = Rota.start_date
else: else:
# ? test if start date is valid # ? test if start date is valid
self.start_date = start_date self.calculated_start_date = self.start_date
if end_date is None: if self.end_date is None:
self.end_date = Rota.rota_end_date self.calculated_end_date = Rota.rota_end_date
else: else:
self.end_date = end_date self.calculated_end_date = self.end_date
# self.end_date = datetime.datetime.strptime(end_date, "%d/%m/%y").date() # self.calculated_end_date = datetime.datetime.strptime(end_date, "%d/%m/%y").date()
if self.end_date > Rota.rota_end_date: if self.calculated_end_date > Rota.rota_end_date:
self.end_date = Rota.rota_end_date self.calculated_end_date = Rota.rota_end_date
days_to_work = (self.end_date - self.start_date).days days_to_work = (self.calculated_end_date - self.calculated_start_date).days
for week, day in Rota.weeks_days_product: for week, day in Rota.weeks_days_product:
date = Rota.week_day_date_map[(week, day)] date = Rota.week_day_date_map[(week, day)]
if date < self.start_date: if date < self.calculated_start_date:
Rota.unavailable_to_work.add((self.id, week, day)) Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[ Rota.unavailable_to_work_reason[
(self.id, week, day) (self.id, week, day)
] = f"START DATE: {self.start_date}" ] = f"START DATE: {self.calculated_start_date}"
if date >= self.end_date: if date >= self.calculated_end_date:
Rota.unavailable_to_work.add((self.id, week, day)) Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[ Rota.unavailable_to_work_reason[
(self.id, week, day) (self.id, week, day)
] = f"END DATE: {self.end_date}" ] = f"END DATE: {self.calculated_end_date}"
if oop is not None: for item in self.oop:
for start_oop, end_oop, oop_name in oop: start_oop = item.start_date
# start_oop, end_oop = oop end_oop = item.end_date
if isinstance(start_oop, datetime.date): oop_name = item.reason
start_oop_date = start_oop # start_oop, end_oop = oop
if isinstance(start_oop, datetime.date):
start_oop_date = start_oop
else:
start_oop_date = datetime.datetime.strptime(
start_oop, "%d/%m/%y"
).date()
if isinstance(end_oop, datetime.date):
end_oop_date = end_oop
else:
end_oop_date = datetime.datetime.strptime(
end_oop, "%d/%m/%y"
).date()
if start_oop_date >= end_oop_date:
raise ValueError("End OOP date must be after start date")
# ignore oops if they finish before the rota (or worker) start date
if end_oop_date > self.calculated_start_date:
if start_oop_date > self.calculated_end_date:
pass
else: else:
start_oop_date = datetime.datetime.strptime( if end_oop_date > Rota.rota_end_date:
start_oop, "%d/%m/%y" end_oop_date = Rota.rota_end_date
).date()
if isinstance(end_oop, datetime.date): if start_oop_date < Rota.start_date:
end_oop_date = end_oop start_oop_date = Rota.start_date
else:
end_oop_date = datetime.datetime.strptime(
end_oop, "%d/%m/%y"
).date()
if start_oop_date >= end_oop_date: oop_length = (end_oop_date - start_oop_date).days
raise ValueError("End OOP date must be after start date") days_to_work = days_to_work - oop_length
# ignore oops if they finish before the rota (or worker) start date days_until_oop = (start_oop_date - Rota.start_date).days
if end_oop_date > self.start_date:
if start_oop_date > self.end_date:
pass
else:
if end_oop_date > Rota.rota_end_date:
end_oop_date = Rota.rota_end_date
if start_oop_date < Rota.start_date: for weeks_days in Rota.weeks_days_product[
start_oop_date = Rota.start_date days_until_oop : days_until_oop + oop_length
]:
week, day = weeks_days
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[
(self.id, week, day)
] = f"OOP ({oop_name})".format(self.oop)
oop_length = (end_oop_date - start_oop_date).days # loop throught dates converting to week / day combination
days_to_work = days_to_work - oop_length for item in self.pref_not_to_work:
days_from_start = (
item.date
- Rota.start_date
).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
# Weight the value to take into account the number of preferences
# 1 is added to the total number of requests to ensure they do not outweight
# a single (or fewer) request(s)
Rota.pref_not_to_work[(self.id, week, day)] = 1 / (
len(self.pref_not_to_work) + 1
)
Rota.pref_not_to_work_reason[(self.id, week, day)] = item.reason
days_until_oop = (start_oop_date - Rota.start_date).days # print(not_available_to_work)
# loop throught dates converting to week / day combination
for item in self.not_available_to_work:
days_from_start = (
item.date
- Rota.start_date
).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason
for weeks_days in Rota.weeks_days_product[ for item in self.work_requests:
days_until_oop : days_until_oop + oop_length
]:
week, day = weeks_days
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[
(self.id, week, day)
] = f"OOP ({oop_name})".format(oop)
if pref_not_to_work is not None:
# loop throught dates converting to week / day combination
for date in pref_not_to_work:
days_from_start = ( days_from_start = (
date item.date
- Rota.start_date - Rota.start_date
).days ).days
week = days_from_start // 7 + 1 week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)] day = Rota.days[(days_from_start % 7)]
# Weight the value to take into account the number of preferences Rota.work_requests.add((self.id, week, day, item.shift))
# 1 is added to the total number of requests to ensure they do not outweight
# a single (or fewer) request(s)
Rota.pref_not_to_work[(self.id, week, day)] = 1 / (
len(pref_not_to_work) + 1
)
if not_available_to_work is not None:
# print(not_available_to_work)
# loop throught dates converting to week / day combination
for date, reason in not_available_to_work:
days_from_start = (
date
- Rota.start_date
).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[(self.id, week, day)] = reason
if work_requests is not None:
for date, shift in work_requests:
days_from_start = (
date
- Rota.start_date
).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
Rota.work_requests.add((self.id, week, day, shift))
# Calculate the proportion of the rota that is being worked # Calculate the proportion of the rota that is being worked
self.proportion_rota_to_work = days_to_work / Rota.rota_days_length self.proportion_rota_to_work = days_to_work / Rota.rota_days_length
@@ -189,15 +221,20 @@ class Worker:
self.fte_adj = self.fte * self.proportion_rota_to_work self.fte_adj = self.fte * self.proportion_rota_to_work
if self.fte_adj > 100: if self.fte_adj > 100:
# Shouldn't happen ? bug if it does
raise ValueError("{} : fte_ajd = {}".format(self.name, self.fte_adj)) raise ValueError("{} : fte_ajd = {}".format(self.name, self.fte_adj))
assert type(nwd) == list, f"nwd must be a list: {nwd=}" # TODO: this has already been validated, consider moving
for item in nwd: self.non_working_day_list = []
match item: for item in self.nwds:
case (nwd_day, start_date, end_date): start_date = Rota.start_date
self.nwd.append((nwd_day, start_date, end_date)) end_date = Rota.rota_end_date
case nwd_day: if item.start_date is not None:
self.nwd.append((nwd_day, Rota.start_date, Rota.rota_end_date)) start_date = item.start_date
if item.end_date is not None:
end_date = item.end_date
self.non_working_day_list.append((item.day, start_date, end_date))
def __lt__(self, other) -> bool: def __lt__(self, other) -> bool:
return (self.site, self.grade, self.fte_adj, self.name) < ( return (self.site, self.grade, self.fte_adj, self.name) < (
@@ -209,11 +246,11 @@ class Worker:
def __str__(self) -> str: def __str__(self) -> str:
nwd = ", ".join([str(i) for i in self.nwd]) if self.nwd is not None else "" nwds = ", ".join([str(i) for i, start, end in self.non_working_day_list]) if self.non_working_day_list is not None else ""
return "{} {} [{}] REMOTE SITE:{}".format( return "{} {} [{}] REMOTE SITE:{}".format(
self.name, self.name,
(self.site, self.grade, self.fte_adj), (self.site, self.grade, self.fte_adj),
nwd, nwds,
self.remote_site, self.remote_site,
) )
+1 -1
View File
@@ -232,7 +232,7 @@ if use_test_workers:
] ]
) )
Rota.add_workers( Rota.add_workers(
[Worker(Rota, i, "Plym {}".format(i), "plymouth", 4, 50) for i in range(9, 17)] [Worker(i, "Plym {}".format(i), "plymouth", 4, 50) for i in range(9, 17)]
) )
Rota.add_workers( Rota.add_workers(
[ [
+8 -8
View File
@@ -16,12 +16,12 @@ def generate_basic_rota(weeks_to_rota=10):
# Add a few workers # Add a few workers
Rota.add_workers([ Rota.add_workers([
Worker(Rota, "worker1", "group1", 1), Worker(name="worker1", site="group1", grade=1),
Worker(Rota, "worker2", "group1", 1), Worker(name="worker2", site="group1", grade=1),
Worker(Rota, "worker3", "group2", 1), Worker(name="worker3", site="group2", grade=1),
#Worker(Rota, "worker4", "group2", 1), #Worker(name="worker4", site="group2", grade=1),
#Worker(Rota, "worker5", "group2", 1), #Worker(name="worker5", site="group2", grade=1),
#Worker(Rota, "worker6", "group2", 1, fte=50), #Worker(name="worker6", site="group2", grade=1, fte=50),
]) ])
return Rota return Rota
@@ -201,7 +201,7 @@ class TestBalancing:
Rota = generate_basic_rota(10) Rota = generate_basic_rota(10)
Rota.add_worker( Rota.add_worker(
Worker(Rota, "worker4", "group3", 1), Worker(name="worker4", site="group3", grade=1),
) )
Rota.add_shifts( Rota.add_shifts(
@@ -242,7 +242,7 @@ class TestBalancing:
Rota = generate_basic_rota(10) Rota = generate_basic_rota(10)
Rota.add_worker( Rota.add_worker(
Worker(Rota, "worker4", "group3", 1), Worker(name="worker4", site="group3", grade=1),
) )
Rota.add_shifts( Rota.add_shifts(
+62 -61
View File
@@ -24,8 +24,8 @@ class TestDemoRota:
) )
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1, nwd=["Mon"]) worker1 = Worker(name="worker1", site="group1", grade=1, nwds=[{"day": "Mon"}])
worker2 = Worker(self.Rota, "worker2", "group1", 1, nwd=["Fri"]) worker2 = Worker(name="worker2", site="group1", grade=1, nwds=[{"day": "Fri"}])
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -36,7 +36,7 @@ class TestDemoRota:
"d", "d",
12.5, 12.5,
days[:5], days[:5],
balance_offset=10, # balance_offset=10,
workers_required=1, workers_required=1,
), ),
SingleShift( SingleShift(
@@ -48,7 +48,7 @@ class TestDemoRota:
), ),
) )
self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.build_and_solve(options={"ratio": 0.10})
self.Rota.export_rota_to_html("nwd") self.Rota.export_rota_to_html("nwd")
@@ -78,8 +78,8 @@ class TestDemoRota:
) )
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1, nwd=["Mon"]) worker1 = Worker(name="worker1", site="group1", grade=1, nwds=[{"day": "Mon"}])
worker2 = Worker(self.Rota, "worker2", "group1", 1, nwd=["Mon"]) worker2 = Worker(name="worker2", site="group1", grade=1, nwds=[{"day": "Mon"}])
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -114,10 +114,16 @@ class TestDemoRota:
d = start_date + datetime.timedelta(weeks=5) d = start_date + datetime.timedelta(weeks=5)
# Add a few workers # Add a few workers
worker1 = Worker( worker1 = Worker(
self.Rota, "worker1", "group1", 1, nwd=[("Mon", start_date, d)] name="worker1",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date": start_date, "end_date": d}],
) )
worker2 = Worker( worker2 = Worker(
self.Rota, "worker2", "group1", 1, nwd=[("Mon", d, self.Rota.rota_end_date)] name="worker2",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date": d, "end_date": self.Rota.rota_end_date}],
) )
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -140,6 +146,21 @@ class TestDemoRota:
assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.status == "ok"
for worker in self.Rota.workers:
shifts = self.Rota.get_worker_shift_list(worker)
# Convert shift to a string representation
shifts_string = "".join([i if i != "" else "-" for i in shifts])
if worker.name == "worker1":
for n in range(0,4):
assert shifts_string[0 + 7*n] == "-"
if worker.name == "worker2":
for n in range(5,9):
assert shifts_string[0 + 7*n] == "-"
def test_nwd_partial_overlay(self): def test_nwd_partial_overlay(self):
# Set up rota # Set up rota
weeks_to_rota = 10 weeks_to_rota = 10
@@ -154,10 +175,16 @@ class TestDemoRota:
d2 = start_date + datetime.timedelta(weeks=6) d2 = start_date + datetime.timedelta(weeks=6)
# Add a few workers # Add a few workers
worker1 = Worker( worker1 = Worker(
self.Rota, "worker1", "group1", 1, nwd=[("Mon", start_date, d2)] name="worker1",
site="group1",
grade=1,
nwds=[{"day":"Mon", "start_date":start_date, "end_date":d2}],
) )
worker2 = Worker( worker2 = Worker(
self.Rota, "worker2", "group1", 1, nwd=[("Mon", d, self.Rota.rota_end_date)] name="worker2",
site="group1",
grade=1,
nwds=[{"day":"Mon", "start_date":d, "end_date":self.Rota.rota_end_date}],
) )
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -190,25 +217,16 @@ class TestDemoRota:
d = start_date + datetime.timedelta(weeks=4) d = start_date + datetime.timedelta(weeks=4)
worker1 = Worker( worker1 = Worker(
self.Rota, "worker1", "group1", 1, nwd=[("Mon", start_date, d)] name="worker1",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date":start_date, "end_date":d}],
) )
worker2 = Worker( worker2 = Worker(name="worker2", site="group1", grade=1,
self.Rota,
"worker2",
"group1",
1,
) )
worker3 = Worker( worker3 = Worker(name="worker3", site="group1", grade=1,
self.Rota,
"worker3",
"group1",
1,
) )
worker4 = Worker( worker4 = Worker(name="worker4", site="group1", grade=1,
self.Rota,
"worker4",
"group1",
1,
) )
self.Rota.add_workers((worker1, worker2, worker3, worker4)) self.Rota.add_workers((worker1, worker2, worker3, worker4))
@@ -255,25 +273,16 @@ class TestDemoRota:
d = start_date + datetime.timedelta(weeks=5) d = start_date + datetime.timedelta(weeks=5)
worker1 = Worker( worker1 = Worker(
self.Rota, "worker1", "group1", 1, nwd=[("Mon", start_date, d)] name="worker1",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date":start_date, "end_date":d}],
) )
worker2 = Worker( worker2 = Worker(name="worker2", site="group1", grade=1,
self.Rota,
"worker2",
"group1",
1,
) )
worker3 = Worker( worker3 = Worker(name="worker3", site="group1", grade=1,
self.Rota,
"worker3",
"group1",
1,
) )
worker4 = Worker( worker4 = Worker(name="worker4", site="group1", grade=1,
self.Rota,
"worker4",
"group1",
1,
) )
self.Rota.add_workers((worker1, worker2, worker3, worker4)) self.Rota.add_workers((worker1, worker2, worker3, worker4))
@@ -303,7 +312,7 @@ class TestDemoRota:
) )
self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("nwd") self.Rota.export_rota_to_html("nwd_block_force_split")
assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.status == "ok"
@@ -333,24 +342,16 @@ class TestDemoRota:
) )
d = start_date + datetime.timedelta(weeks=5) d = start_date + datetime.timedelta(weeks=5)
worker1 = Worker( worker1 = Worker(name="worker1", site="group1", grade=1,
self.Rota, nwds=[
"worker1", {"day": "Mon", "start_date":d, "end_date":self.Rota.rota_end_date},
"group1", {"day": "Tue", "start_date":d, "end_date":self.Rota.rota_end_date},
1, {"day": "Wed", "start_date":d, "end_date":self.Rota.rota_end_date},
nwd=[ {"day": "Thu", "start_date":d, "end_date":self.Rota.rota_end_date},
("Mon", d, self.Rota.rota_end_date), {"day": "Fri", "start_date":d, "end_date":self.Rota.rota_end_date},
("Tue", d, self.Rota.rota_end_date),
("Wed", d, self.Rota.rota_end_date),
("Thu", d, self.Rota.rota_end_date),
("Fri", d, self.Rota.rota_end_date),
], ],
) )
worker2 = Worker( worker2 = Worker(name="worker2", site="group1", grade=1,
self.Rota,
"worker2",
"group1",
1,
) )
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -396,8 +397,8 @@ class TestDemoRota:
else: else:
assert week == "dddddww" assert week == "dddddww"
for week in weeks_from_list(shifts_string[:7 * 5 ]): for week in weeks_from_list(shifts_string[: 7 * 5]):
if worker.name == "worker1": if worker.name == "worker1":
assert week == "dddddww" assert week == "dddddww"
else: else:
assert week == "-----ww" assert week == "-----ww"
+181
View File
@@ -0,0 +1,181 @@
import pytest
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
import datetime
from rota.workers import Worker
def setup_basic_rota():
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
return RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
class TestRemoteRotas:
def test_remote_fail(self):
Rota = setup_basic_rota()
Rota.add_workers(
[Worker(
name="worker1", site="group1", grade=1, remote_site="group1"
),
Worker(
name="worker2", site="group2", grade=1, remote_site="group2"
),]
)
Rota.add_shifts(
SingleShift(
("group1", "group2"),
"a",
12.5,
days,
workers_required=1,
balance_offset=100,
force_as_block=False,
constraints=[("require_remote_site_presence_week", ("group2", 2))],
),
)
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status == "warning"
def test_remote_pass_week(self):
Rota = setup_basic_rota()
Rota.constraint_options["balance_bank_holidays"] = False
Rota.constraint_options["balance_shifts"] = False
Rota.constraint_options["balance_shifts_quadratic"] = False
Rota.constraint_options["balance_shifts_over_workers"] =True
Rota.constraint_options["balance_blocks"] = False
Rota.constraint_options["balance_weekends"] = False
Rota.constraint_options["balance_nights"] = False
Rota.constraint_options["minimse_shift_diffs"] = False
Rota.add_workers(
[Worker(
name="worker1", site="group2", grade=1, remote_site="group1"
),
Worker(
name="worker2", site="group2", grade=1, remote_site="group2"
),Worker(
name="worker3", site="group2", grade=1, remote_site="group2"
),]
)
Rota.add_shifts(
SingleShift(
("group1", "group2"),
"a",
12.5,
days[:3],
workers_required=1,
balance_offset=10,
force_as_block=True,
constraints=[("require_remote_site_presence_week", ("group2", 1))],
),
SingleShift(
("group1", "group2"),
"b",
12.5,
days[3:],
workers_required=1,
balance_offset=10,
force_as_block=False,
#constraints=[("require_remote_site_presence_week", ("group2", 1))],
#constraint=["balance_shifts_over_workers"]
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("remote_pass_week")
assert Rota.results.solver.status == "ok"
for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
# Convert shift to a string representation
shifts_string = "".join([i if i != "" else "-" for i in shifts])
if worker.site == "group1":
assert "a" not in shifts_string
total_shifts = Rota.get_workers_total_shifts()
shift_range = max(total_shifts.values()) - min(total_shifts.values())
assert shift_range <= 1
def test_remote_pass(self):
Rota = setup_basic_rota()
Rota.constraint_options["balance_bank_holidays"] = False
Rota.constraint_options["balance_shifts"] = False
Rota.constraint_options["balance_shifts_quadratic"] = False
Rota.constraint_options["balance_shifts_over_workers"] =True
Rota.constraint_options["balance_blocks"] = False
Rota.constraint_options["balance_weekends"] = False
Rota.constraint_options["balance_nights"] = False
Rota.constraint_options["minimse_shift_diffs"] = False
Rota.add_workers(
[Worker(
name="worker1", site="group2", grade=1, remote_site="group1"
),
Worker(
name="worker2", site="group2", grade=1, remote_site="group2"
),Worker(
name="worker3", site="group2", grade=1, remote_site="group2"
),]
)
Rota.add_shifts(
SingleShift(
("group1", "group2"),
"a",
12.5,
days[:3],
workers_required=1,
balance_offset=10,
force_as_block=False,
constraints=[("require_remote_site_presence", ("group2", 1))],
),
SingleShift(
("group1", "group2"),
"b",
12.5,
days[3:],
workers_required=1,
balance_offset=10,
force_as_block=False,
#constraints=[("require_remote_site_presence_week", ("group2", 1))],
#constraint=["balance_shifts_over_workers"]
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("remote_pass")
assert Rota.results.solver.status == "ok"
for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
# Convert shift to a string representation
shifts_string = "".join([i if i != "" else "-" for i in shifts])
if worker.site == "group1":
assert "a" not in shifts_string
total_shifts = Rota.get_workers_total_shifts()
shift_range = max(total_shifts.values()) - min(total_shifts.values())
assert shift_range <= 1
+94 -57
View File
@@ -24,20 +24,20 @@ class TestDemoRota:
Rota.constraint_options["max_shifts_per_month"] = 31 Rota.constraint_options["max_shifts_per_month"] = 31
Rota.constraint_options["constrain_time_off_after_nights"] = False Rota.constraint_options["constrain_time_off_after_nights"] = False
Rota.constraint_options["balance_nights_across_site"] = False Rota.constraint_options["balance_nights_across_site"] = False
Rota.constraint_options["balance_shifts"] = True Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.constraint_options["balance_shifts_over_workers"] = True Rota.constraint_options["balance_shifts_over_workers"] = True
Rota.constraint_options["balance_nights"] = False Rota.constraint_options["balance_nights"] = False
Rota.constraint_options["minimise_shift_diffs"] = False Rota.constraint_options["minimise_shift_diffs"] = False
Rota.constraint_options["balance_blocks"] = False Rota.constraint_options["balance_blocks"] = False
# Rota.constraint_options["balance_weekends"] = False Rota.constraint_options["balance_bank_holidays"] = False
# Add a few workers # Add a few workers
worker1 = Worker(Rota, "worker1", "group1", 1) worker1 = Worker(name= "worker1", site= "group1", grade= 1)
worker2 = Worker(Rota, "worker2", "group1", 1) worker2 = Worker(name= "worker2", site= "group1", grade= 1)
worker3 = Worker(Rota, "worker3", "group1", 1) worker3 = Worker(name= "worker3", site= "group1", grade= 1)
worker4 = Worker(Rota, "worker4", "group1", 1) worker4 = Worker(name= "worker4", site= "group1", grade= 1)
worker5 = Worker(Rota, "worker5", "group2", 1, fte=60) worker5 = Worker(name= "worker5", site= "group2", grade= 1, fte=60)
worker6 = Worker(Rota, "worker6", "group2", 1, fte=40) worker6 = Worker(name= "worker6", site= "group2", grade= 1, fte=40)
Rota.add_workers((worker1, worker2, worker3, worker4, worker5, worker6)) Rota.add_workers((worker1, worker2, worker3, worker4, worker5, worker6))
@@ -207,14 +207,14 @@ class TestDemoRotaNights:
# Rota.constraint_options["balance_weekends"] = False # Rota.constraint_options["balance_weekends"] = False
# Add a few workers # Add a few workers
worker1 = Worker(Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(Rota, "worker3", "group1", 1) worker3 = Worker(name="worker3", site="group1", grade=1)
worker4 = Worker(Rota, "worker4", "group1", 1) worker4 = Worker(name="worker4", site="group1", grade=1)
worker5 = Worker(Rota, "worker5", "group2", 1, fte=60) worker5 = Worker(name="worker5", site="group2", grade=1, fte=60)
worker6 = Worker(Rota, "worker6", "group2", 1, fte=40) worker6 = Worker(name="worker6", site="group2", grade=1, fte=40)
worker7 = Worker(Rota, "worker7", "group2", 1) worker7 = Worker(name="worker7", site="group2", grade=1)
worker8 = Worker(Rota, "worker8", "group2", 1) worker8 = Worker(name="worker8", site="group2", grade=1)
Rota.add_workers( Rota.add_workers(
(worker1, worker2, worker3, worker4, worker5, worker6, worker7, worker8) (worker1, worker2, worker3, worker4, worker5, worker6, worker7, worker8)
@@ -307,14 +307,14 @@ class TestDemoRotaClear:
) )
Rota.constraint_options["max_shifts_per_month"] = 20 Rota.constraint_options["max_shifts_per_month"] = 20
worker1 = Worker(Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(Rota, "worker3", "group1", 1) worker3 = Worker(name="worker3", site="group1", grade=1)
worker4 = Worker(Rota, "worker4", "group1", 1) worker4 = Worker(name="worker4", site="group1", grade=1)
worker5 = Worker(Rota, "worker5", "group2", 1, fte=60) worker5 = Worker(name="worker5", site="group2", grade=1, fte=60)
worker6 = Worker(Rota, "worker6", "group2", 1, fte=40) worker6 = Worker(name="worker6", site="group2", grade=1, fte=40)
worker7 = Worker(Rota, "worker7", "group2", 1) worker7 = Worker(name="worker7", site="group2", grade=1)
worker8 = Worker(Rota, "worker8", "group2", 1) worker8 = Worker(name="worker8", site="group2", grade=1)
Rota.add_workers( Rota.add_workers(
(worker1, worker2, worker3, worker4, worker5, worker6, worker7, worker8) (worker1, worker2, worker3, worker4, worker5, worker6, worker7, worker8)
@@ -393,8 +393,8 @@ class TestDemoRotaShiftConstraints:
) )
# Rota.constraint_options["max_shifts_per_week"] = 5 # Rota.constraint_options["max_shifts_per_week"] = 5
# Rota.constraint_options["max_shifts_per_month"] = 20 # Rota.constraint_options["max_shifts_per_month"] = 20
worker1 = Worker(Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
Rota.add_workers((worker1, worker2)) Rota.add_workers((worker1, worker2))
Rota.add_shifts( Rota.add_shifts(
@@ -409,7 +409,7 @@ class TestDemoRotaShiftConstraints:
) )
Rota3Workers = deepcopy(Rota) Rota3Workers = deepcopy(Rota)
worker3 = Worker(Rota3Workers, "worker3", "group1", 1) worker3 = Worker(name="worker3", site="group1", grade=1)
Rota3Workers.add_worker(worker3) Rota3Workers.add_worker(worker3)
@@ -449,7 +449,7 @@ class TestDemoRotaShiftConstraints:
def test_max_shifts_per_week_extra_worker_pass(self): def test_max_shifts_per_week_extra_worker_pass(self):
self.Rota3Workers.constraint_options["max_shifts_per_week"] = 3 self.Rota3Workers.constraint_options["max_shifts_per_week"] = 3
# worker3 = Worker(self.Rota, "worker3", "group1", 1) # worker3 = Worker(name="worker3", site="group1", grade=1)
# self.Rota.add_worker(worker3) # self.Rota.add_worker(worker3)
self.Rota3Workers.build_and_solve() self.Rota3Workers.build_and_solve()
@@ -466,12 +466,12 @@ class TestDemoRotaBalanceShiftSites:
) )
# Rota.constraint_options["max_shifts_per_week"] = 5 # Rota.constraint_options["max_shifts_per_week"] = 5
# Rota.constraint_options["max_shifts_per_month"] = 20 # Rota.constraint_options["max_shifts_per_month"] = 20
worker1 = Worker(Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(Rota, "worker3", "group2", 1) worker3 = Worker(name="worker3", site="group2", grade=1)
worker4 = Worker(Rota, "worker4", "group2", 1) worker4 = Worker(name="worker4", site="group2", grade=1)
worker5 = Worker(Rota, "worker5", "group3", 1) worker5 = Worker(name="worker5", site="group3", grade=1)
worker6 = Worker(Rota, "worker6", "group3", 1) worker6 = Worker(name="worker6", site="group3", grade=1)
Rota.add_workers((worker1, worker2, worker3, worker4)) Rota.add_workers((worker1, worker2, worker3, worker4))
Rota.add_workers((worker5, worker6)) Rota.add_workers((worker5, worker6))
@@ -564,6 +564,8 @@ class TestDemoRotaBalanceShiftSites:
self.Rota.constraint_options["constrain_time_off_after_nights"] = False self.Rota.constraint_options["constrain_time_off_after_nights"] = False
self.Rota.build_and_solve(options={"ratio": 0.1}) self.Rota.build_and_solve(options={"ratio": 0.1})
print("HELLO")
group_workers = self.Rota.get_workers_by_group() group_workers = self.Rota.get_workers_by_group()
for group in group_workers: for group in group_workers:
shift_patterns = [] shift_patterns = []
@@ -589,12 +591,12 @@ class TestLimitConstraints:
) )
# Rota.constraint_options["max_shifts_per_week"] = 5 # Rota.constraint_options["max_shifts_per_week"] = 5
# Rota.constraint_options["max_shifts_per_month"] = 20 # Rota.constraint_options["max_shifts_per_month"] = 20
worker1 = Worker(Rota, "worker1", "group1", 2, remote_site="group1") worker1 = Worker(name="worker1", site="group1", grade=2, remote_site="group1")
worker2 = Worker(Rota, "worker2", "group1", 2, remote_site="group1") worker2 = Worker(name="worker2", site="group1", grade=2, remote_site="group1")
worker3 = Worker(Rota, "worker3", "group1", 2, remote_site="group1") worker3 = Worker(name="worker3", site="group1", grade=2, remote_site="group1")
worker4 = Worker(Rota, "worker4", "group3", 3, remote_site="group2") worker4 = Worker(name="worker4", site="group3", grade=3, remote_site="group2")
worker5 = Worker(Rota, "worker5", "group3", 3, remote_site="group2") worker5 = Worker(name="worker5", site="group3", grade=3, remote_site="group2")
worker6 = Worker(Rota, "worker6", "group3", 3, remote_site="group2") worker6 = Worker(name="worker6", site="group3", grade=3, remote_site="group2")
Rota.add_workers((worker1, worker2, worker3, worker4)) Rota.add_workers((worker1, worker2, worker3, worker4))
Rota.add_workers((worker5, worker6)) Rota.add_workers((worker5, worker6))
@@ -847,18 +849,18 @@ class TestLimitConstraints:
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.Rota.build_and_solve(options={"ratio": 0.1}) self.Rota.build_and_solve(options={"ratio": 0.1})
def test_constraint_require_remote_site_presence(self): def test_constraint_require_remote_site_presence_week(self):
self.Rota.shifts = [] self.Rota.shifts = []
self.Rota.add_shifts( self.Rota.add_shifts(
SingleShift( SingleShift(
("group1", "group2", "group3"), ("group1", "group2", "group3"),
"weekday_night", "night",
12.5, 12.5,
days, days,
balance_offset=40, balance_offset=40,
workers_required=2, workers_required=2,
force_as_block=True, force_as_block=True,
constraints=[("require_remote_site_presence", ("group1", 2))], constraints=[("require_remote_site_presence_week", ("group1", 2))],
), ),
) )
self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.build_and_solve(options={"ratio": 0.00})
@@ -876,29 +878,33 @@ class TestLimitConstraints:
if group != "group1": if group != "group1":
limit = 0 limit = 0
for day_shifts in zipped_lists: for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit assert day_shifts.count("night") == limit
assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal" assert self.Rota.results.solver.termination_condition == "optimal"
def test_constraint_require_remote_site_presence2(self): def test_constraint_require_remote_site_presence_week2(self):
self.Rota.constraint_options["minimise_shift_diffs"] = False
self.Rota.constraint_options["balance_shifts_quadratic"] = False
self.Rota.constraint_options["balance_shift"] = False
self.Rota.constraint_options["balance_nights_across_sites"] = True
self.Rota.shifts = [] self.Rota.shifts = []
self.Rota.add_shifts( self.Rota.add_shifts(
SingleShift( SingleShift(
("group1", "group2", "group3"), ("group1", "group2", "group3"),
"weekday_night", "night",
12.5, 12.5,
days, days,
balance_offset=40, balance_offset=20,
workers_required=3, workers_required=3,
force_as_block=True, force_as_block=True,
constraints=[("require_remote_site_presence", ("group2", 2))], constraints=[("require_remote_site_presence_week", ("group2", 2))],
), ),
) )
self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("remote2") self.Rota.export_rota_to_html("remote2")
group_workers = self.Rota.get_workers_by_group() group_workers = self.Rota.get_workers_by_remote_group()
for group in group_workers: for group in group_workers:
shift_patterns = [] shift_patterns = []
for w in group_workers[group]: for w in group_workers[group]:
@@ -908,11 +914,42 @@ class TestLimitConstraints:
if group == "group2": if group == "group2":
for day_shifts in zipped_lists: for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") >= 2 assert day_shifts.count("night") >= 2
assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal" assert self.Rota.results.solver.termination_condition == "optimal"
def test_constraint_require_remote_site_presence_week3(self):
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(
("group1", "group2", "group3"),
"night",
12.5,
days,
balance_offset=20,
workers_required=3,
force_as_block=True,
constraints=[("require_remote_site_presence_week", ("group2", 1))],
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("remote3")
group_workers = self.Rota.get_workers_by_remote_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
shift_patterns.append(self.Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
if group == "group2":
for day_shifts in zipped_lists:
assert day_shifts.count("night") >= 1
assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal"
# class TestNoWorkerRota: # class TestNoWorkerRota:
# weeks_to_rota = 10 # weeks_to_rota = 10
@@ -942,10 +979,10 @@ class TestNightUnavailable:
# Add a few workers # Add a few workers
worker1 = Worker( worker1 = Worker(
Rota, "worker1", "group1", 1, not_available_to_work=((datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(), "****"),) name="worker1", site="group1", grade=1, not_available_to_work=[{ "date": datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(), "reason" : "****" },]
) )
worker2 = Worker( worker2 = Worker(
Rota, "worker2", "group1", 1, not_available_to_work=((datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(), "****"),) name="worker2", site="group1", grade=1, not_available_to_work=[ { "date": datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(), "reason": "****" } ]
) )
Rota.add_workers((worker1, worker2)) Rota.add_workers((worker1, worker2))
@@ -1007,10 +1044,10 @@ class TestNightUnavailable:
def test_assign_split(self): def test_assign_split(self):
self.Rota.shifts = [] self.Rota.shifts = []
worker3 = Worker( worker3 = Worker(
self.Rota, "worker3", "group1", 1, not_available_to_work=((datetime.datetime.strptime("13/03/22", "%d/%m/%y").date(), "****"),) name="worker3", site="group1", grade=1, not_available_to_work=({"date":datetime.datetime.strptime("13/03/22", "%d/%m/%y").date()},)
) )
worker4 = Worker( worker4 = Worker(
self.Rota, "worker4", "group1", 1, not_available_to_work=((datetime.datetime.strptime("12/03/22", "%d/%m/%y").date(), "****"),) name="worker4", site="group1", grade=1, not_available_to_work=({"date":datetime.datetime.strptime("12/03/22", "%d/%m/%y").date()},)
) )
self.Rota.add_workers((worker3, worker4)) self.Rota.add_workers((worker3, worker4))
@@ -1033,10 +1070,10 @@ class TestNightUnavailable:
# def test_assign_split_night_constraint(self): # def test_assign_split_night_constraint(self):
# self.Rota.shifts = [] # self.Rota.shifts = []
# worker3 = Worker( # worker3 = Worker(
# self.Rota, "worker3", "group1", 1, not_available_to_work=((datetime.datetime.strptime("13/03/22", "%d/%m/%y").date(), "****"),) # self.Rota, name="worker3", site="group1", grade=1, not_available_to_work=((datetime.datetime.strptime("13/03/22", "%d/%m/%y").date(), "****"),)
# ) # )
# worker4 = Worker( # worker4 = Worker(
# self.Rota, "worker4", "group1", 1, not_available_to_work=((datetime.datetime.strptime("12/03/22", "%d/%m/%y").date(), "****"),) # self.Rota, name="worker4", site="group1", grade=1, not_available_to_work=((datetime.datetime.strptime("12/03/22", "%d/%m/%y").date(), "****"),)
# ) # )
# self.Rota.add_workers((worker3, worker4)) # self.Rota.add_workers((worker3, worker4))
# #
@@ -1052,4 +1089,4 @@ class TestNightUnavailable:
if __name__ == "__main__": if __name__ == "__main__":
t = TestLimitConstraints() t = TestLimitConstraints()
t.test_constraint_require_remote_site_presence() t.test_constraint_require_remote_site_presence_week()
+3 -6
View File
@@ -18,8 +18,8 @@ def generate_basic_rota(weeks_to_rota=10):
# Add a few workers # Add a few workers
Rota.add_workers( Rota.add_workers(
[ [
Worker(Rota, "worker1", "group1", 1), Worker(name="worker1", site="group1", grade=1),
Worker(Rota, "worker2", "group1", 1), Worker(name="worker2", site="group1", grade=1),
] ]
) )
@@ -43,10 +43,7 @@ class TestShifts:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3",
"group1",
1,
), ),
) )
+19 -19
View File
@@ -23,10 +23,10 @@ class TestDemoRota:
self.Rota.constraint_options["balance_weekends"] = True self.Rota.constraint_options["balance_weekends"] = True
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(self.Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(self.Rota, "worker3", "group1", 1) worker3 = Worker(name="worker3", site="group1", grade=1)
worker4 = Worker(self.Rota, "worker4", "group1", 1) worker4 = Worker(name="worker4", site="group1", grade=1)
self.Rota.add_workers((worker1, worker2, worker3, worker4)) self.Rota.add_workers((worker1, worker2, worker3, worker4))
@@ -86,11 +86,11 @@ class TestDemoRota:
self.Rota.constraint_options["balance_weekends"] = True self.Rota.constraint_options["balance_weekends"] = True
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(self.Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(self.Rota, "worker3", "group1", 1) worker3 = Worker(name="worker3", site="group1", grade=1)
worker4 = Worker(self.Rota, "worker4", "group1", 1) worker4 = Worker(name="worker4", site="group1", grade=1)
worker5 = Worker(self.Rota, "worker5", "group1", 1) worker5 = Worker(name="worker5", site="group1", grade=1)
self.Rota.add_workers((worker1, worker2, worker3, worker4, worker5)) self.Rota.add_workers((worker1, worker2, worker3, worker4, worker5))
@@ -151,8 +151,8 @@ class TestDemoRota:
self.Rota.constraint_options["prevent_monday_and_tuesday_after_full_weekends"] = ["group1",] self.Rota.constraint_options["prevent_monday_and_tuesday_after_full_weekends"] = ["group1",]
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(self.Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -203,8 +203,8 @@ class TestDemoRota:
self.Rota.constraint_options["prevent_monday_and_tuesday_after_full_weekends"] = ["group1",] self.Rota.constraint_options["prevent_monday_and_tuesday_after_full_weekends"] = ["group1",]
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(self.Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -251,8 +251,8 @@ class TestDemoRota:
self.Rota.constraint_options["prevent_monday_after_full_weekends"] = ["group1",] self.Rota.constraint_options["prevent_monday_after_full_weekends"] = ["group1",]
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(self.Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -310,8 +310,8 @@ class TestDemoRota:
self.Rota.constraint_options["prevent_monday_after_full_weekends"] = ["group1",] self.Rota.constraint_options["prevent_monday_after_full_weekends"] = ["group1",]
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(self.Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
@@ -360,8 +360,8 @@ class TestDemoRota:
self.Rota.constraint_options["prevent_monday_after_full_weekends"] = ["group1",] self.Rota.constraint_options["prevent_monday_after_full_weekends"] = ["group1",]
# Add a few workers # Add a few workers
worker1 = Worker(self.Rota, "worker1", "group1", 1) worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(self.Rota, "worker2", "group1", 1) worker2 = Worker(name="worker2", site="group1", grade=1)
self.Rota.add_workers((worker1, worker2)) self.Rota.add_workers((worker1, worker2))
+27 -63
View File
@@ -18,8 +18,8 @@ def generate_basic_rota(weeks_to_rota=10):
# Add a few workers # Add a few workers
Rota.add_workers( Rota.add_workers(
[ [
Worker(Rota, "worker1", "group1", 1), Worker(name="worker1", site="group1", grade=1),
Worker(Rota, "worker2", "group1", 1), Worker(name="worker2", site="group1", grade=1),
] ]
) )
@@ -43,11 +43,8 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3", pref_not_to_work=[{"date": d} for d in date_generator(datetime.date(2022, 3, 7), 5)],
"group1",
1,
pref_not_to_work=(list(date_generator(datetime.date(2022, 3, 7), 5))),
), ),
) )
@@ -86,11 +83,8 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3", pref_not_to_work=[{"date": d} for d in date_generator(datetime.date(2022, 3, 7), 55)],
"group1",
1,
pref_not_to_work=(list(date_generator(datetime.date(2022, 3, 7), 55))),
), ),
) )
@@ -127,11 +121,8 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3", pref_not_to_work=[{"date": d} for d in date_generator(datetime.date(2022, 3, 7), 70)],
"group1",
1,
pref_not_to_work=(list(date_generator(datetime.date(2022, 3, 7), 70))),
), ),
) )
@@ -170,13 +161,10 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3",
"group1",
1,
not_available_to_work=( not_available_to_work=(
[ [
(i, "NOT AROUND") {"date": i, "reason": "NOT AROUND"}
for i in list(date_generator(datetime.date(2022, 3, 7), 23)) for i in list(date_generator(datetime.date(2022, 3, 7), 23))
] ]
), ),
@@ -218,13 +206,10 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3",
"group1",
1,
not_available_to_work=( not_available_to_work=(
[ [
(i, "NOT AROUND") {"date": i, "reason": "NOT AROUND"}
for i in list(date_generator(datetime.date(2022, 3, 7), 25)) for i in list(date_generator(datetime.date(2022, 3, 7), 25))
] ]
), ),
@@ -257,17 +242,14 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3",
"group1",
1,
not_available_to_work=( not_available_to_work=(
[ [
(i, "NOT AROUND") {"date":i, "reason":"NOT AROUND"}
for i in list(date_generator(datetime.date(2022, 3, 7), 23)) for i in list(date_generator(datetime.date(2022, 3, 7), 23))
] ]
), ),
pref_not_to_work=[datetime.date(2022, 5, 15)], pref_not_to_work=[{"date":datetime.date(2022, 5, 15)}],
), ),
) )
@@ -309,25 +291,19 @@ class TestWorkerRequests:
Rota.add_workers( Rota.add_workers(
[Worker( [Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3",
"group1",
1,
not_available_to_work=( not_available_to_work=(
[ [
(i, "NOT AROUND") {"date": i, "reason": "NOT AROUND"}
for i in list(date_generator(datetime.date(2022, 3, 7), 25)) for i in list(date_generator(datetime.date(2022, 3, 7), 25))
] ]
), ),
), ),
Worker( Worker(
Rota, name="worker4", site="group1", grade=1,
"worker4",
"group1",
1,
not_available_to_work=( not_available_to_work=(
[ [
(i, "NOT AROUND") {"date": i, "reason": "NOT AROUND"}
for i in list(date_generator(datetime.date(2022, 3, 7), 25)) for i in list(date_generator(datetime.date(2022, 3, 7), 25))
] ]
), ),
@@ -359,25 +335,19 @@ class TestWorkerRequests:
Rota.add_workers( Rota.add_workers(
[Worker( [Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3",
"group1",
1,
work_requests=( work_requests=(
[ [
(i, "a") {"date": i, "shift": "a"}
for i in list(date_generator(datetime.date(2022, 3, 7), 35)) for i in list(date_generator(datetime.date(2022, 3, 7), 35))
] ]
), ),
), ),
Worker( Worker(
Rota, name="worker4", site="group1", grade=1,
"worker4",
"group1",
1,
work_requests=( work_requests=(
[ [
(i, "a") {"date": i, "shift": "a"}
for i in list(date_generator(datetime.date(2022, 3, 7), 35)) for i in list(date_generator(datetime.date(2022, 3, 7), 35))
] ]
), ),
@@ -419,13 +389,10 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker3", site="group1", grade=1,
"worker3",
"group1",
1,
work_requests=( work_requests=(
[ [
(i, "b") {"date": i, "shift": "b"}
for i in list(date_generator(datetime.date(2022, 3, 7), 35)) for i in list(date_generator(datetime.date(2022, 3, 7), 35))
] ]
), ),
@@ -482,13 +449,10 @@ class TestWorkerRequests:
Rota.add_worker( Rota.add_worker(
Worker( Worker(
Rota, name="worker4", site="group1", grade=1,
"worker4",
"group1",
1,
work_requests=( work_requests=(
[ [
(i, "d") {"date": i, "shift": "d"}
for i in list(date_generator(datetime.date(2022, 3, 7), 35)) for i in list(date_generator(datetime.date(2022, 3, 7), 35))
] ]
), ),
+106 -78
View File
@@ -2,7 +2,6 @@ import pytest
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
import datetime import datetime
from rota.workers import Worker from rota.workers import Worker
@@ -41,10 +40,10 @@ class TestWorkers:
Rota = setup_basic_rota() Rota = setup_basic_rota()
workers = [] workers = []
workers.append(Worker(Rota, "worker1", "group1", 1, fte=100)) workers.append(Worker(name="worker1", site="group1", grade=1, fte=100))
workers.append(Worker(Rota, "worker1", "group1", 1, fte=100)) workers.append(Worker(name="worker1", site="group1", grade=1, fte=100))
workers.append(Worker(Rota, "worker3", "group2", 1, fte=50)) workers.append(Worker(name="worker3", site="group2", grade=1, fte=50))
workers.append(Worker(Rota, "worker4", "group2", 1, fte=50)) workers.append(Worker(name="worker4", site="group2", grade=1, fte=50))
Rota.add_workers(workers) Rota.add_workers(workers)
@@ -81,36 +80,48 @@ class TestWorkers:
workers = [] workers = []
workers.append( workers.append(
Worker(Rota, "worker1", "group1", 1, fte=100, end_date=worker1_end_date) Worker(
) name="worker1",
workers.append( site="group1",
Worker(Rota, "worker2", "group1", 1, fte=100, start_date=worker2_start_date) grade=1,
fte=100,
end_date=worker1_end_date,
)
) )
workers.append( workers.append(
Worker( Worker(
Rota, name="worker2",
"worker3", site="group1",
"group2", grade=1,
1, fte=100,
start_date=worker2_start_date,
)
)
workers.append(
Worker(
name="worker3",
site="group2",
grade=1,
fte=100, fte=100,
start_date=worker3_start_date, start_date=worker3_start_date,
end_date=worker3_end_date, end_date=worker3_end_date,
) )
) )
workers.append(Worker(Rota, "worker4", "group2", 1, fte=50)) workers.append(Worker(name="worker4", site="group2", grade=1, fte=50))
workers.append(Worker(Rota, "worker5", "group2", 1, fte=50, nwd=["Mon", "Tue"])) workers.append(
Worker(name="worker5", site="group2", grade=1, fte=50, nwd=["Mon", "Tue"])
)
workers.append( workers.append(
Worker( Worker(
Rota, name="worker6",
"worker6", site="group3",
"group3", grade=1,
1,
fte=100, fte=100,
start_date=start_date + datetime.timedelta(weeks=weeks_to_rota), start_date=start_date + datetime.timedelta(weeks=weeks_to_rota),
) )
) )
workers.append( workers.append(
Worker(Rota, "worker7", "group3", 1, fte=50, end_date=start_date) Worker(name="worker7", site="group3", grade=1, fte=50, end_date=start_date)
) )
Rota.add_workers(workers) Rota.add_workers(workers)
@@ -131,6 +142,7 @@ class TestWorkers:
# All workers should have an adjusted fte of 50 % # All workers should have an adjusted fte of 50 %
for worker in Rota.get_workers(): for worker in Rota.get_workers():
# Except those in group3
if worker.site == "group3": if worker.site == "group3":
assert worker.fte_adj == 0 assert worker.fte_adj == 0
assert Rota.get_worker_shift_list(worker).count("d") == 0 assert Rota.get_worker_shift_list(worker).count("d") == 0
@@ -145,13 +157,20 @@ class TestWorkers:
assert Rota.get_worker_shift_list(worker).count("d") == 10 assert Rota.get_worker_shift_list(worker).count("d") == 10
if worker.name == "worker1": if worker.name == "worker1":
assert Rota.get_worker_shift_list_string(worker).endswith("-"*(Rota.rota_days_length//2)) assert Rota.get_worker_shift_list_string(worker).endswith(
"-" * (Rota.rota_days_length // 2)
)
if worker.name == "worker2": if worker.name == "worker2":
assert Rota.get_worker_shift_list_string(worker).startswith("-"*(Rota.rota_days_length//2)) assert Rota.get_worker_shift_list_string(worker).startswith(
"-" * (Rota.rota_days_length // 2)
)
if worker.name == "worker3": if worker.name == "worker3":
assert Rota.get_worker_shift_list_string(worker).startswith("-"*(Rota.rota_days_length//4)) assert Rota.get_worker_shift_list_string(worker).startswith(
assert Rota.get_worker_shift_list_string(worker).endswith("-"*(Rota.rota_days_length//4)) "-" * (Rota.rota_days_length // 4)
)
assert Rota.get_worker_shift_list_string(worker).endswith(
"-" * (Rota.rota_days_length // 4)
)
assert Rota.results.solver.status == "ok" assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal" assert Rota.results.solver.termination_condition == "optimal"
@@ -168,87 +187,96 @@ class TestWorkers:
) )
worker1_oop = [ worker1_oop = [
( {
start_date, "start_date": start_date,
start_date + datetime.timedelta(weeks=weeks_to_rota / 2), "end_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
"First half OOP", "reason": "First half OOP",
) }
] ]
worker2_oop = [ worker2_oop = [
( {
start_date + datetime.timedelta(weeks=weeks_to_rota / 2), "start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
Rota.rota_end_date, "end_date": Rota.rota_end_date,
"First half OOP", "reason": "First half OOP",
) }
] ]
worker3_start_date = start_date + datetime.timedelta(weeks=weeks_to_rota / 4) worker3_start_date = start_date + datetime.timedelta(weeks=weeks_to_rota / 4)
worker3_oop = [ worker3_oop = [
( {
start_date + datetime.timedelta(weeks=weeks_to_rota / 2), "start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
start_date + datetime.timedelta(weeks=3 * weeks_to_rota / 4), "end_date": start_date + datetime.timedelta(weeks=3 * weeks_to_rota / 4),
"Quarter oop", "reason": "Quarter oop",
) }
] ]
worker4_oop = [ worker4_oop = [
( {
start_date, "start_date": start_date,
start_date + datetime.timedelta(weeks=weeks_to_rota / 4), "end_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 4),
"Quarter oop", "reason": "Quarter oop",
), },
( {
start_date + datetime.timedelta(weeks=weeks_to_rota / 2), "start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
start_date "end_date": start_date
+ datetime.timedelta(weeks=weeks_to_rota / 2) + datetime.timedelta(weeks=weeks_to_rota / 2)
+ datetime.timedelta(weeks=weeks_to_rota / 4), + datetime.timedelta(weeks=weeks_to_rota / 4),
"Quarter oop 2", "reason": "Quarter oop 2",
), },
] ]
workers = [] workers = []
workers.append(Worker(Rota, "worker1", "group1", 1, fte=100, oop=worker1_oop)) workers.append(
workers.append(Worker(Rota, "worker2", "group1", 1, fte=100, oop=worker2_oop)) Worker(name="worker1", site="group1", grade=1, fte=100, oop=worker1_oop)
)
workers.append(
Worker(name="worker2", site="group1", grade=1, fte=100, oop=worker2_oop)
)
workers.append( workers.append(
Worker( Worker(
Rota, name="worker3",
"worker3", site="group2",
"group2", grade=1,
1,
fte=100, fte=100,
start_date=worker3_start_date, start_date=worker3_start_date,
oop=worker3_oop, oop=worker3_oop,
) )
) )
workers.append(Worker(Rota, "worker4", "group2", 1, oop=worker4_oop)) workers.append(Worker(name="worker4", site="group2", grade=1, oop=worker4_oop))
workers.append(Worker(Rota, "worker5", "group2", 1, fte=50, nwd=["Mon", "Tue"])) workers.append(
Worker(name="worker5", site="group2", grade=1, fte=50, nwd=["Mon", "Tue"])
)
worker6_oop = [ worker6_oop = [
( {
start_date, "start_date": start_date,
start_date + datetime.timedelta(weeks=weeks_to_rota / 6), "end_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 6),
"oop", "reason": "oop",
), },
( {
start_date + datetime.timedelta(weeks=weeks_to_rota / 2), "start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
start_date + datetime.timedelta(weeks=weeks_to_rota / 2) "end_date": start_date
+ datetime.timedelta(weeks=weeks_to_rota / 2)
+ datetime.timedelta(weeks=weeks_to_rota / 3), + datetime.timedelta(weeks=weeks_to_rota / 3),
" oop 2", "reason": " oop 2",
), },
] ]
worker7_oop = [ worker7_oop = [
( {
start_date-datetime.timedelta(weeks=1), "start_date": start_date - datetime.timedelta(weeks=1),
Rota.rota_end_date, "end_date": Rota.rota_end_date,
"oop", "reason": "oop",
), },
] ]
workers.append(
workers.append(Worker(Rota, "worker6", "group2", 1, fte=100, oop=worker6_oop)) Worker(name="worker6", site="group2", grade=1, fte=100, oop=worker6_oop)
workers.append(Worker(Rota, "worker7", "group3", 1, fte=100, oop=worker7_oop)) )
workers.append(
Worker(name="worker7", site="group3", grade=1, fte=100, oop=worker7_oop)
)
Rota.add_workers(workers) Rota.add_workers(workers)
@@ -274,8 +302,8 @@ class TestWorkers:
assert worker.fte_adj == pytest.approx(50, 2) assert worker.fte_adj == pytest.approx(50, 2)
if worker.fte == 100: if worker.fte == 100:
assert worker.proportion_rota_to_work == pytest.approx(0.5, 0.2) assert worker.proportion_rota_to_work == pytest.approx(0.5, 0.2)
assert worker.days_to_work == pytest.approx(35, 1) assert worker.days_to_work == pytest.approx(35, 1)
assert Rota.get_worker_shift_list(worker).count("d") in (8, 9) assert Rota.get_worker_shift_list(worker).count("d") in (8, 9)