Add support for requesting shifts

This commit is contained in:
Ross
2020-05-28 18:16:46 +01:00
parent 469674546d
commit 74484db6c6
6 changed files with 171 additions and 26 deletions
+50 -11
View File
@@ -33,7 +33,8 @@ rota_collections = {
"no weekends": deepcopy(Rota), "no weekends": deepcopy(Rota),
"no twighlights": deepcopy(Rota), "no twighlights": deepcopy(Rota),
"no nights": deepcopy(Rota), "no nights": deepcopy(Rota),
"nights + proc twighlights + normal weekends": deepcopy(Rota), "nights + proc twighlights3 + normal weekends": deepcopy(Rota),
"nights + proc twighlights4 + normal weekends": deepcopy(Rota),
"nights + proc twighlights + proc weekends": deepcopy(Rota), "nights + proc twighlights + proc weekends": deepcopy(Rota),
} }
@@ -211,7 +212,7 @@ rota_collections['no nights'].add_shifts(
#SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True), #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
) )
rota_collections['nights + proc twighlights + normal weekends'].add_shifts( rota_collections['nights + proc twighlights4 + normal weekends'].add_shifts(
SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=4), SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=4),
SingleShift(("exeter", ), SingleShift(("exeter", ),
"weekend_exeter", "weekend_exeter",
@@ -257,17 +258,55 @@ rota_collections['nights + proc twighlights + normal weekends'].add_shifts(
constraints=["night"]), constraints=["night"]),
) )
rota_collections['nights + proc twighlights + proc weekends'].add_shifts( rota_collections['nights + proc twighlights3 + normal weekends'].add_shifts(
SingleShift((sites), SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
"proc_twilight", SingleShift(("exeter", ),
12.5, "weekend_exeter",
days[:5],
workers_required=3),
SingleShift((sites),
"proc_weekends",
12.5, 12.5,
days[5:], days[5:],
workers_required=3), assign_as_block=True),
SingleShift(("truro", ),
"weekend_truro",
12.5,
days[5:],
assign_as_block=True),
SingleShift(("torquay", ),
"weekend_torquay",
12.5,
days[5:],
assign_as_block=True),
SingleShift(("plymouth", ),
"weekend_plymouth",
8,
days[5:],
workers_required=2,
assign_as_block=True),
#SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
SingleShift((sites),
"night_weekday",
12.25,
days[:4],
balance_offset=5,
balance_weighting=1,
workers_required=3,
force_as_block=False,
rota_on_nwds=True,
constraints=["night"]),
SingleShift((sites),
"night_weekend",
12.25,
days[4:],
balance_offset=4,
balance_weighting=1,
workers_required=3,
force_as_block=False,
rota_on_nwds=True,
constraints=["night"]),
)
rota_collections['nights + proc twighlights + proc weekends'].add_shifts(
SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
SingleShift((sites), "proc_weekends", 12.5, days[5:], workers_required=3),
#SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True), #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
SingleShift((sites), SingleShift((sites),
"night_weekday", "night_weekday",
+5 -1
View File
@@ -74,7 +74,11 @@ tables.each((n, table) => {
whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3 whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
} }
// 0.5 for twilights // 0.5 for twilights
if(el.includes("twilight")) { days_lost = days_lost + shifts.count(el) / 2 } if(el.includes("twilight")) {
twilight_days_lost = shifts.count(el) / 2
days_lost = days_lost + twilight_days_lost
}
}); });
+59 -10
View File
@@ -21,6 +21,14 @@ days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
# weeks_days_product = list(itertools.product(weeks, days)) # weeks_days_product = list(itertools.product(weeks, days))
from govuk_bank_holidays.bank_holidays import BankHolidays
bank_holidays = BankHolidays()
bank_holiday_map = {}
for bank_holiday in bank_holidays.get_holidays():
bank_holiday_map[bank_holiday['date']] = bank_holiday['title']
# see BankHolidays source file for more methods and arguments…
class SingleShift(object): class SingleShift(object):
"""Class to hold all details for a shift""" """Class to hold all details for a shift"""
@@ -89,6 +97,11 @@ class RotaBuilder(object):
self.weeks_days_product = list(itertools.product(self.weeks, days)) self.weeks_days_product = list(itertools.product(self.weeks, days))
if start_date.weekday() != 0:
raise ValueError(
"Start date {} must be a Mon (not a {})".format(
start_date.isoformat(), days[start_date.weekday()]))
self.start_date = start_date self.start_date = start_date
self.rota_days_length = len(self.weeks) * 7 self.rota_days_length = len(self.weeks) * 7
self.rota_end_date = self.start_date + datetime.timedelta( self.rota_end_date = self.start_date + datetime.timedelta(
@@ -96,6 +109,8 @@ class RotaBuilder(object):
self.unavailable_to_work = set() self.unavailable_to_work = set()
self.pref_not_to_work = {} self.pref_not_to_work = {}
self.work_requests = set()
self.work_requests_map = {}
self.workers = [] self.workers = []
@@ -338,14 +353,25 @@ class RotaBuilder(object):
return 0 return 0
return 1 return 1
#print(self.unavailable_to_work)
self.model.available = Param( self.model.available = Param(
((worker.id, week, day) for worker in self.workers ((worker.id, week, day) for worker in self.workers
for week, day in self.get_week_day_combinations()), for week, day in self.get_week_day_combinations()),
initialize=availability_init, initialize=availability_init,
) )
def work_request_init(model, wid, week, day, shift):
if (wid, week, day, shift) in self.work_requests:
self.work_requests_map[(wid, week, day)] = shift
print((wid, week, day))
return 1
return 0
self.model.work_requests = Param(
((worker.id, week, day, shift) for worker in self.workers
for week, day, shift in self.get_all_shiftname_combinations()),
initialize=work_request_init,
)
def pref_init(model, wid, week, day): def pref_init(model, wid, week, day):
if (wid, week, day) in self.pref_not_to_work: if (wid, week, day) in self.pref_not_to_work:
return self.pref_not_to_work[(wid, week, day)] return self.pref_not_to_work[(wid, week, day)]
@@ -943,13 +969,21 @@ class RotaBuilder(object):
weekend_shift_balancing = 0 weekend_shift_balancing = 0
preference_constant = 10 preference_constant = 10
# Preferences # Preferences (not to work)
preferences = sum( preferences = sum(
self.model.pref_not_to_work[worker.id, week, day] * self.model.pref_not_to_work[worker.id, week, day] *
self.model.works[worker.id, week, day, shift] * self.model.works[worker.id, week, day, shift] *
preference_constant for worker in self.workers preference_constant for worker in self.workers
for week, day, shift in self.get_all_shiftname_combinations()) for week, day, shift in self.get_all_shiftname_combinations())
# Work requsets
work_request_constant = 1000
work_requests = sum(
self.model.work_requests[worker.id, week, day, shift] *
self.model.works[worker.id, week, day, shift] *
work_request_constant for worker in self.workers
for week, day, shift in self.get_all_shiftname_combinations())
# # Spread nights # # Spread nights
if self.constraint_options["balance_nights_across_sites"]: if self.constraint_options["balance_nights_across_sites"]:
@@ -974,7 +1008,7 @@ class RotaBuilder(object):
#return weekend_shift_balancing + blocks_balancing #return weekend_shift_balancing + blocks_balancing
#return shift_balancing + preferences + blocks_balancing #return shift_balancing + preferences + blocks_balancing
#return shift_balancing + preferences + nights_site_balancing + blocks_balancing #return shift_balancing + preferences + nights_site_balancing + blocks_balancing
return shift_balancing + night_shift_balancing + preferences + nights_site_balancing + blocks_balancing return shift_balancing + night_shift_balancing + preferences + nights_site_balancing + blocks_balancing - work_requests
# add objective function to the model. rule (pass function) or expr (pass expression directly) # add objective function to the model. rule (pass function) or expr (pass expression directly)
self.model.obj = Objective(rule=obj_rule, sense=minimize) self.model.obj = Objective(rule=obj_rule, sense=minimize)
@@ -1406,27 +1440,42 @@ class RotaResults(object):
n = 0 n = 0
for week, day in self.rota.get_week_day_combinations(): for week, day in self.rota.get_week_day_combinations():
d = self.rota.start_date + datetime.timedelta(n) d = self.rota.start_date + datetime.timedelta(n)
n = n + 1 n = n + 1
a = "-" a = "-"
shift_name = "" shift_name = ""
title = "{} ({})".format(shift_name, d)
for shift in self.rota.get_shift_names_by_day(day): for shift in self.rota.get_shift_names_by_day(day):
if model.works[worker.id, week, day, shift].value == 1: if model.works[worker.id, week, day, shift].value == 1:
shifts.append(shift) shifts.append(shift)
a = shift[0] a = shift[0]
shift_name = shift shift_name = shift
break break
css_class = "" css_class = day
if model.available[worker.id, week, day] > 0: if model.available[worker.id, week, day] > 0:
available = True available = True
else: else:
available = False available = False
css_class = "unavailable" css_class = "unavailable"
if worker.name == "Delilah Trimmer":
pass bank_holiday = ""
if d in bank_holiday_map:
css_class = " ".join((css_class, "bank-holiday"))
bank_holiday = " data-bank-holiday='{}'".format(
bank_holiday_map[d])
requests = ""
if (worker.id, week, day) in self.rota.work_requests_map:
css_class = " ".join((css_class, "shift-requested"))
title = " ".join((title, "[REQUESTED]"))
requests = " data-shift-request='{}'".format(
self.rota.work_requests_map[(worker.id, week, day)])
w.append( w.append(
"<td title='{} ({})' class='rota-day {}' data-shift='{}' data-available='{}' data-date='{}' data-week='{}' data-day='{}'>{}</td>" "<td title='{}' class='rota-day {}' data-shift='{}' data-available='{}' data-date='{}' data-week='{}' data-day='{}'{}{}>{}</td>"
.format(shift_name, d, css_class, shift_name, available, d, .format(title, css_class, shift_name, available, d, week,
week, day, a)) day, requests, bank_holiday, a))
shift_count = "" shift_count = ""
for s in set(shifts): for s in set(shifts):
+28 -2
View File
@@ -17,10 +17,10 @@ import operator
use_neos = False use_neos = False
weeks_to_rota = 12 weeks_to_rota = 8
time_to_run = 1200 time_to_run = 1200
allow = 3000 allow = 500
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth") sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
@@ -217,6 +217,32 @@ else:
# Rota.add_worker( # Rota.add_worker(
# Worker(Rota, n, name, site.lower(), int(grade[2]), 100, # Worker(Rota, n, name, site.lower(), int(grade[2]), 100,
# )) # ))
if name.startswith(("Alex", "Michal", "Ross")):
Rota.add_worker(
Worker(Rota,
n,
name,
site.lower(),
int(grade[2]),
int(fte),
nwds,
end_date,
oop,
work_requests=(("2020/09/07", "night_weekday"), )))
continue
if "Salma" in name:
Rota.add_worker(
Worker(Rota,
n,
name,
site.lower(),
int(grade[2]),
int(fte),
nwds,
end_date,
oop,
work_requests=(("2020/09/11", "night_weekend"), )))
continue
Rota.add_worker( Rota.add_worker(
Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte), Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte),
nwds, end_date, oop)) nwds, end_date, oop))
+20 -1
View File
@@ -58,6 +58,22 @@ tr:hover .unavailable{
background-color: lightgray; background-color: lightgray;
} }
.Sat {
/* opacity: 30%; */
/* background-color: lightsteelblue; */
border-left: solid 1px lightsteelblue;
}
.Sun {
border-right: solid 1px lightsteelblue;
}
.bank-holiday {
border: solid 1px blue;
}
th.worker { th.worker {
max-width: 100%; max-width: 100%;
width: 200px; width: 200px;
@@ -79,6 +95,9 @@ th.worker {
.unavailable { .unavailable {
color: lightgray; color: lightgray;
} }
.shift-requested {
color: green;
}
.worker-summary { .worker-summary {
position: absolute; position: absolute;
@@ -105,4 +124,4 @@ th.worker {
.training-days-lost { .training-days-lost {
padding-top: 5px; padding-top: 5px;
} }
+9 -1
View File
@@ -15,7 +15,8 @@ class Worker:
nwd=None, nwd=None,
end_date=None, end_date=None,
oop=None, oop=None,
pref_not_to_work=None): pref_not_to_work=None,
work_requests=None,):
self.id = id self.id = id
self.name = name self.name = name
self.site = site self.site = site
@@ -81,6 +82,13 @@ class Worker:
# a single (or fewer) request(s) # a single (or fewer) request(s)
Rota.pref_not_to_work[(self.id, week, day)] = 1 / (len(pref_not_to_work) + 1) Rota.pref_not_to_work[(self.id, week, day)] = 1 / (len(pref_not_to_work) + 1)
if work_requests is not None:
for date, shift in work_requests:
days_from_start = (datetime.datetime.strptime(date, "%Y/%m/%d").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))
self.proportion_rota_to_work = days_to_work / Rota.rota_days_length self.proportion_rota_to_work = days_to_work / Rota.rota_days_length