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)
+2
View File
@@ -2,3 +2,5 @@ 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 = []
+121 -84
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,94 +14,125 @@ 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
shift: str = ""
class PreferenceNotToWork(BaseModel):
date: datetime.date
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 # Grade are equivalent to roles, by keeping them integer defining model
# rules is easier. # rules is easier.
self.grade = grade grade: int
self.fte = fte # We can either have a user generated ID
self.nwd = [] 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
end_oop = item.end_date
oop_name = item.reason
# start_oop, end_oop = oop # start_oop, end_oop = oop
if isinstance(start_oop, datetime.date): if isinstance(start_oop, datetime.date):
start_oop_date = start_oop start_oop_date = start_oop
@@ -118,8 +152,8 @@ class Worker:
raise ValueError("End OOP date must be after start date") raise ValueError("End OOP date must be after start date")
# ignore oops if they finish before the rota (or worker) start date # ignore oops if they finish before the rota (or worker) start date
if end_oop_date > self.start_date: if end_oop_date > self.calculated_start_date:
if start_oop_date > self.end_date: if start_oop_date > self.calculated_end_date:
pass pass
else: else:
if end_oop_date > Rota.rota_end_date: if end_oop_date > Rota.rota_end_date:
@@ -140,13 +174,12 @@ class Worker:
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"OOP ({oop_name})".format(oop) ] = f"OOP ({oop_name})".format(self.oop)
if pref_not_to_work is not None:
# loop throught dates converting to week / day combination # loop throught dates converting to week / day combination
for date in pref_not_to_work: for item in self.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
@@ -155,31 +188,30 @@ class Worker:
# 1 is added to the total number of requests to ensure they do not outweight # 1 is added to the total number of requests to ensure they do not outweight
# a single (or fewer) request(s) # a single (or fewer) request(s)
Rota.pref_not_to_work[(self.id, week, day)] = 1 / ( Rota.pref_not_to_work[(self.id, week, day)] = 1 / (
len(pref_not_to_work) + 1 len(self.pref_not_to_work) + 1
) )
Rota.pref_not_to_work_reason[(self.id, week, day)] = item.reason
if not_available_to_work is not None:
# print(not_available_to_work) # print(not_available_to_work)
# loop throught dates converting to week / day combination # loop throught dates converting to week / day combination
for date, reason in not_available_to_work: for item in self.not_available_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)]
Rota.unavailable_to_work.add((self.id, week, day)) Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[(self.id, week, day)] = reason Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason
if work_requests is not None: for item in self.work_requests:
for date, shift in work_requests:
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)]
Rota.work_requests.add((self.id, week, day, shift)) Rota.work_requests.add((self.id, week, day, item.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(
+60 -59
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))
+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))
] ]
), ),
+103 -75
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",
site="group1",
grade=1,
fte=100,
end_date=worker1_end_date,
) )
workers.append(
Worker(Rota, "worker2", "group1", 1, fte=100, start_date=worker2_start_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)