Add support for requesting shifts
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user