go
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import csv
|
||||
|
||||
|
||||
def load_leave(Rota, file_name="leave.csv"):
|
||||
|
||||
shifts = Rota.get_shift_names()
|
||||
|
||||
with open(file_name, newline="") as f:
|
||||
reader = csv.reader(f, delimiter=',', quotechar='|')
|
||||
|
||||
leave = {}
|
||||
requests = {}
|
||||
site_prefs = {}
|
||||
|
||||
order = []
|
||||
|
||||
n = 0
|
||||
for row in reader:
|
||||
r = row[2:]
|
||||
# header, extract names
|
||||
if n == 0:
|
||||
names = r
|
||||
order = names
|
||||
for name in names:
|
||||
leave[name] = []
|
||||
requests[name] = []
|
||||
site_prefs[name] = []
|
||||
|
||||
# preferred site
|
||||
if n == 5:
|
||||
for i in range(len(r)):
|
||||
pref = r[i].lower()
|
||||
|
||||
if pref == "1":
|
||||
site_prefs[order[i]].append(pref)
|
||||
else:
|
||||
site_prefs[order[i]].append("0")
|
||||
|
||||
|
||||
if n > 6:
|
||||
for i in range(len(r)):
|
||||
lower_item = r[i].lower()
|
||||
if lower_item != "":
|
||||
# This may be easier to do as a dict
|
||||
if lower_item in shifts:
|
||||
requests[order[i]].append((row[0], lower_item))
|
||||
|
||||
else:
|
||||
leave[order[i]].append((row[0], lower_item))
|
||||
|
||||
n = n + 1
|
||||
|
||||
return (leave, requests, site_prefs)
|
||||
|
||||
|
||||
#load_leave()
|
||||
@@ -17,7 +17,7 @@ import operator
|
||||
|
||||
use_neos = False
|
||||
|
||||
weeks_to_rota = 26
|
||||
weeks_to_rota = 52
|
||||
|
||||
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
|
||||
|
||||
@@ -29,13 +29,13 @@ Rota = RotaBuilder(datetime.date(2020, 9, 7),
|
||||
|
||||
rota_collections = {
|
||||
"current": deepcopy(Rota),
|
||||
"extended proc nights only": deepcopy(Rota),
|
||||
"no weekends": deepcopy(Rota),
|
||||
"no twighlights": deepcopy(Rota),
|
||||
"no nights": deepcopy(Rota),
|
||||
"nights + proc twighlights3 + normal weekends": deepcopy(Rota),
|
||||
"nights + proc twighlights4 + normal weekends": deepcopy(Rota),
|
||||
"nights + proc twighlights + proc weekends": deepcopy(Rota),
|
||||
# "extended proc nights only": deepcopy(Rota),
|
||||
# "no weekends": deepcopy(Rota),
|
||||
# "no twighlights": deepcopy(Rota),
|
||||
# "no nights": deepcopy(Rota),
|
||||
# "nights + proc twighlights3 + normal weekends": deepcopy(Rota),
|
||||
# "nights + proc twighlights4 + normal weekends": deepcopy(Rota),
|
||||
# "nights + proc twighlights + proc weekends": deepcopy(Rota),
|
||||
}
|
||||
|
||||
rota_collections['current'].add_shifts(
|
||||
@@ -86,249 +86,257 @@ rota_collections['current'].add_shifts(
|
||||
constraints=["night"]),
|
||||
)
|
||||
|
||||
rota_collections['extended proc nights only'].add_shifts(
|
||||
#SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
|
||||
SingleShift((sites),
|
||||
"night_weekday",
|
||||
13,
|
||||
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",
|
||||
13,
|
||||
days[4:],
|
||||
balance_offset=4,
|
||||
balance_weighting=1,
|
||||
workers_required=3,
|
||||
force_as_block=False,
|
||||
rota_on_nwds=True,
|
||||
constraints=["night"]),
|
||||
)
|
||||
# rota_collections['extended proc nights only'].add_shifts(
|
||||
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
|
||||
# SingleShift((sites),
|
||||
# "night_weekday",
|
||||
# 13,
|
||||
# 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",
|
||||
# 13,
|
||||
# days[4:],
|
||||
# balance_offset=4,
|
||||
# balance_weighting=1,
|
||||
# workers_required=3,
|
||||
# force_as_block=False,
|
||||
# rota_on_nwds=True,
|
||||
# constraints=["night"]),
|
||||
# )
|
||||
|
||||
rota_collections['no weekends'].add_shifts(
|
||||
SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||
SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
||||
SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||
SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
||||
#SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
|
||||
SingleShift((sites),
|
||||
"night_weekday",
|
||||
13,
|
||||
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",
|
||||
13,
|
||||
days[4:],
|
||||
balance_offset=4,
|
||||
balance_weighting=1,
|
||||
workers_required=3,
|
||||
force_as_block=False,
|
||||
rota_on_nwds=True,
|
||||
constraints=["night"]),
|
||||
)
|
||||
# rota_collections['no weekends'].add_shifts(
|
||||
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
||||
# #SingleShift((sites), "night", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
|
||||
# SingleShift((sites),
|
||||
# "night_weekday",
|
||||
# 13,
|
||||
# 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",
|
||||
# 13,
|
||||
# days[4:],
|
||||
# balance_offset=4,
|
||||
# balance_weighting=1,
|
||||
# workers_required=3,
|
||||
# force_as_block=False,
|
||||
# rota_on_nwds=True,
|
||||
# constraints=["night"]),
|
||||
# )
|
||||
|
||||
rota_collections['no twighlights'].add_shifts(
|
||||
SingleShift(("exeter", ),
|
||||
"weekend_exeter",
|
||||
12.5,
|
||||
days[5:],
|
||||
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",
|
||||
13,
|
||||
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",
|
||||
13,
|
||||
days[4:],
|
||||
balance_offset=4,
|
||||
balance_weighting=1,
|
||||
workers_required=3,
|
||||
force_as_block=False,
|
||||
rota_on_nwds=True,
|
||||
constraints=["night"]),
|
||||
)
|
||||
# rota_collections['no twighlights'].add_shifts(
|
||||
# SingleShift(("exeter", ),
|
||||
# "weekend_exeter",
|
||||
# 12.5,
|
||||
# days[5:],
|
||||
# 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",
|
||||
# 13,
|
||||
# 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",
|
||||
# 13,
|
||||
# days[4:],
|
||||
# balance_offset=4,
|
||||
# balance_weighting=1,
|
||||
# workers_required=3,
|
||||
# force_as_block=False,
|
||||
# rota_on_nwds=True,
|
||||
# constraints=["night"]),
|
||||
# )
|
||||
|
||||
rota_collections['no nights'].add_shifts(
|
||||
SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||
SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
||||
SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||
SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
||||
SingleShift(("exeter", ),
|
||||
"weekend_exeter",
|
||||
12.5,
|
||||
days[5:],
|
||||
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),
|
||||
)
|
||||
# rota_collections['no nights'].add_shifts(
|
||||
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("exeter", ),
|
||||
# "weekend_exeter",
|
||||
# 12.5,
|
||||
# days[5:],
|
||||
# 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),
|
||||
# )
|
||||
|
||||
rota_collections['nights + proc twighlights4 + normal weekends'].add_shifts(
|
||||
SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=4),
|
||||
SingleShift(("exeter", ),
|
||||
"weekend_exeter",
|
||||
12.5,
|
||||
days[5:],
|
||||
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 twighlights4 + normal weekends'].add_shifts(
|
||||
# SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=4),
|
||||
# SingleShift(("exeter", ),
|
||||
# "weekend_exeter",
|
||||
# 12.5,
|
||||
# days[5:],
|
||||
# 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 twighlights3 + normal weekends'].add_shifts(
|
||||
SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
|
||||
SingleShift(("exeter", ),
|
||||
"weekend_exeter",
|
||||
12.5,
|
||||
days[5:],
|
||||
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 twighlights3 + normal weekends'].add_shifts(
|
||||
# SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
|
||||
# SingleShift(("exeter", ),
|
||||
# "weekend_exeter",
|
||||
# 12.5,
|
||||
# days[5:],
|
||||
# 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_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_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"]),
|
||||
# )
|
||||
|
||||
|
||||
load_leave = True
|
||||
|
||||
if load_leave:
|
||||
import leave
|
||||
|
||||
leave = leave.load_leave()
|
||||
|
||||
# Import trainee data
|
||||
import csv
|
||||
@@ -347,16 +355,24 @@ with open('trainees.csv', newline='') as f:
|
||||
if fte == "0":
|
||||
continue
|
||||
|
||||
nwds = nwd.split(",").title() if nwd else None
|
||||
nwds = nwd.split("/") if nwd else None
|
||||
end_date = end_date if end_date else None
|
||||
oop = oop.split("-") if oop else None
|
||||
|
||||
|
||||
if load_leave:
|
||||
if name not in leave:
|
||||
raise ValueError("{} not found it leave requests".format(name))
|
||||
|
||||
leave_requests = [i[0] for i in leave[name] if i[0] != ""]
|
||||
|
||||
#print(nwds, end_date, oop)
|
||||
for rot in rota_collections:
|
||||
|
||||
Rota = rota_collections[rot]
|
||||
Rota.add_worker(
|
||||
Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte),
|
||||
nwds, end_date, oop))
|
||||
nwds, end_date, oop, not_available_to_work=leave_requests))
|
||||
|
||||
with open("rotas.html", "w") as f:
|
||||
f.write("""<html>
|
||||
@@ -412,19 +428,19 @@ for rot in rota_collections:
|
||||
|
||||
ResultsHolder.export_rota_to_csv("rotas/" + rot)
|
||||
with open("rotas.html", "a") as f:
|
||||
f.write("----------------------------")
|
||||
f.write("<hr>")
|
||||
f.write("<h2>{}</h2>".format(rot))
|
||||
f.write("----------------------------")
|
||||
#f.write("----------------------------")
|
||||
f.write("\n\n")
|
||||
|
||||
for s in Rota.shifts:
|
||||
f.write("<pre>{}</pre>".format(s.get_shift_summary()))
|
||||
f.write("<div id='{}-{}-shift-summary' class='shift-summary'>{}</div>".format(rot, s.name, s.get_shift_summary()))
|
||||
f.write("\n")
|
||||
f.write("\n\n")
|
||||
|
||||
f.write(ResultsHolder.get_worker_timetable_html(table_name=rot))
|
||||
f.write("\n\n")
|
||||
f.write("<pre>{}</pre>".format(worker_shift_summary))
|
||||
f.write("<pre class='worker-shift-summary'>{}</pre>".format(worker_shift_summary))
|
||||
f.write("\n\n\n")
|
||||
#workers_no_pref = ResultsHolder.get_no_preference(Rota.model.no_pref) # list with the non-satisfied workers (work on Sat but not on Sun)
|
||||
#worker_night_block = ResultsHolder.get_night_blocks()
|
||||
|
||||
@@ -14,149 +14,274 @@ Object.defineProperties(Array.prototype, {
|
||||
}
|
||||
});
|
||||
|
||||
const mean = arr => arr.reduce( ( p, c ) => p + c, 0 ) / arr.length;
|
||||
const mean = arr => arr.reduce((p, c) => p + c, 0) / arr.length;
|
||||
|
||||
tables = $(".table-div");
|
||||
|
||||
tables.each((n, table) => {
|
||||
console.log(table)
|
||||
|
||||
rows = $(table).find(".worker-row");
|
||||
console.log(rows)
|
||||
function generateExtra() {
|
||||
$(".auto-generated").remove();
|
||||
global_shifts = new Set();
|
||||
|
||||
workers = {};
|
||||
tables.each((n, table) => {
|
||||
|
||||
rows.each((n, tr) => {
|
||||
rows = $(table).find(".worker-row");
|
||||
|
||||
//console.log(tr);
|
||||
workers = {};
|
||||
|
||||
worker_td = $(tr).find("td:first");
|
||||
rows.each((n, tr) => {
|
||||
|
||||
worker = worker_td.attr("data-worker");
|
||||
|
||||
shift_tds = $(tr).find(".rota-day");
|
||||
shifts = []
|
||||
worker_td = $(tr).find("td:first");
|
||||
|
||||
shift_tds.each((n, td) => {
|
||||
shifts.push($(td).attr("data-shift"));
|
||||
})
|
||||
worker = worker_td.attr("data-worker");
|
||||
|
||||
shifts_unique = new Set(shifts);
|
||||
shifts_unique.delete("");
|
||||
//console.log(shifts_unique);
|
||||
shift_tds = $(tr).find(".rota-day");
|
||||
shifts = []
|
||||
|
||||
weekends_worked = 0;
|
||||
shift_tds.each((n, td) => {
|
||||
shifts.push($(td).attr("data-shift"));
|
||||
})
|
||||
|
||||
weeks = shift_tds.length / 7;
|
||||
shifts_unique = new Set(shifts);
|
||||
shifts_unique.delete("");
|
||||
global_shifts = new Set([...global_shifts, ...shifts_unique]);
|
||||
|
||||
whole_weekends_worked = 0;
|
||||
weekends_worked = 0;
|
||||
|
||||
for(var i = 1; i <= weeks; i++) {
|
||||
//console.log($(tr).find(`[data-week='${i}'][data-day='Sat']`));
|
||||
if($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" || $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") { weekends_worked = weekends_worked + 1; }
|
||||
if($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" && $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") { whole_weekends_worked = whole_weekends_worked + 1; }
|
||||
}
|
||||
weeks = shift_tds.length / 7;
|
||||
|
||||
shift_counts = {};
|
||||
|
||||
total_shifts = 0;
|
||||
days_lost = 0;
|
||||
shifts_unique.forEach(el => {
|
||||
//console.log(el);
|
||||
shift_counts[el] = shifts.count(el);
|
||||
total_shifts = total_shifts + shifts.count(el);
|
||||
|
||||
// 5 days for weekday nights
|
||||
if(el == "night_weekday") { days_lost = days_lost + shifts.count(el) / 4 * 5 }
|
||||
// 4 days for weekend nights
|
||||
if(el == "night_weekend") {
|
||||
days_lost = days_lost + shifts.count(el) / 3 * 4
|
||||
whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
|
||||
}
|
||||
// 0.5 for twilights
|
||||
if(el.includes("twilight")) {
|
||||
twilight_days_lost = shifts.count(el) / 2
|
||||
days_lost = days_lost + twilight_days_lost
|
||||
whole_weekends_worked = 0;
|
||||
|
||||
for(var i = 1; i <= weeks; i++) {
|
||||
if($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" || $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") { weekends_worked = weekends_worked + 1; }
|
||||
if($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" && $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") { whole_weekends_worked = whole_weekends_worked + 1; }
|
||||
}
|
||||
|
||||
});
|
||||
shift_counts = {};
|
||||
|
||||
// Also 1 day for a whole weekend
|
||||
days_lost = days_lost + whole_weekends_worked
|
||||
console.log(worker, whole_weekends_worked);
|
||||
total_shifts = 0;
|
||||
days_lost = 0;
|
||||
shifts_unique.forEach(el => {
|
||||
shift_counts[el] = shifts.count(el);
|
||||
total_shifts = total_shifts + shifts.count(el);
|
||||
|
||||
//days_lost = days_lost + whole_weekends_worked;
|
||||
//console.log(shift_counts);
|
||||
// why like this?
|
||||
if(el == "night_weekend") {
|
||||
whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
|
||||
}
|
||||
|
||||
workers[worker] = {
|
||||
"site": worker_td.attr("data-site"),
|
||||
"nwds": worker_td.attr("data-nwds"),
|
||||
"fte": worker_td.attr("data-fte_adj"),
|
||||
"end_date": worker_td.attr("data-end_date"),
|
||||
"shifts": shifts,
|
||||
"shift_types": shifts_unique,
|
||||
"weekends_worked": weekends_worked,
|
||||
"shift_counts": shift_counts,
|
||||
"days_lost": days_lost,
|
||||
//"shifts"
|
||||
}
|
||||
if($("#global-settings").length == 1) {
|
||||
|
||||
sc = JSON.stringify(shift_counts);
|
||||
|
||||
worker_td.after(`<div class='worker-summary'><span>Total shifts: ${total_shifts}, </span><span>Weekends: ${weekends_worked}, </span><span>Training days lost: ${days_lost}, </span><span class=''>${sc}</span></div>`)
|
||||
|
||||
})
|
||||
|
||||
sites = {};
|
||||
sites_days_lost = {}
|
||||
for (const worker in workers) {
|
||||
//console.log(worker, workers[worker]);
|
||||
w = workers[worker];
|
||||
if (w.fte == 100) {
|
||||
//console.log(w.shift_types);
|
||||
for (let shift of w.shift_types) {
|
||||
//console.log(shift);
|
||||
if (!(w.site in sites)) {
|
||||
sites[w.site] = {};
|
||||
|
||||
}
|
||||
if (!(shift in sites[w.site])) {
|
||||
sites[w.site][shift] = [w.shift_counts[shift]];
|
||||
} else {
|
||||
sites[w.site][shift].push(w.shift_counts[shift]);
|
||||
}
|
||||
}
|
||||
if (!(w.site in sites_days_lost)) {
|
||||
sites_days_lost[w.site] = [w.days_lost];
|
||||
|
||||
} else {
|
||||
sites_days_lost[w.site].push(w.days_lost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate site shift summaries
|
||||
$(table).append("<div class='site-summary'>Average shifts for a 100% FTE trainee</div>");
|
||||
|
||||
for (const site in sites) {
|
||||
console.log(site, $("div"));
|
||||
$(table).find(".site-summary").append(`<div class='site site-${site}'><span class='site-name'>${site}</span></div>`);
|
||||
|
||||
for (const shift in sites[site]) {
|
||||
console.log(sites[site][shift]);
|
||||
average = mean(sites[site][shift])
|
||||
$(table).find(`.site-${site}`).append(`<div>${shift}: ${average.toFixed(2)}</div>`);
|
||||
}
|
||||
|
||||
days_lost_avg = mean(sites_days_lost[site]);
|
||||
$(table).find(`.site-${site}`).append(`<div class='training-days-lost'>Training days lost: ${days_lost_avg.toFixed(2)}</div>`);
|
||||
}
|
||||
n = $(`#${el}-tdl`).val();
|
||||
// console.log("SHIT", `${el}-tdl`, n)
|
||||
if(!isNaN(n)) {
|
||||
days_lost = days_lost + shifts.count(el) * n;
|
||||
}
|
||||
|
||||
|
||||
$(table).before($("<button>Toggle Summary</button>").click(() => {
|
||||
$(table).find(".rota-day").toggle();
|
||||
$(table).find(".worker-summary").toggle();
|
||||
}
|
||||
|
||||
}))
|
||||
});
|
||||
// // 5 days for weekday nights
|
||||
// if(el == "night_weekday") { days_lost = days_lost + shifts.count(el) / 4 * 5 }
|
||||
// // 4 days for weekend nights
|
||||
// if(el == "night_weekend") {
|
||||
// days_lost = days_lost + shifts.count(el) / 3 * 4
|
||||
// //whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
|
||||
// }
|
||||
// // 0.5 for twilights
|
||||
// if(el.includes("twilight")) {
|
||||
// twilight_days_lost = shifts.count(el) / 2
|
||||
// days_lost = days_lost + twilight_days_lost
|
||||
|
||||
// }
|
||||
|
||||
});
|
||||
|
||||
// Also 1 day for a whole weekend
|
||||
days_lost = days_lost + whole_weekends_worked
|
||||
|
||||
//days_lost = days_lost + whole_weekends_worked;
|
||||
//console.log(shift_counts);
|
||||
|
||||
nwds = worker_td.attr("data-nwds");
|
||||
|
||||
console.log(nwds);
|
||||
|
||||
if(nwds != "None") {
|
||||
nwds.split(", ").forEach((nwd) => {
|
||||
console.log(worker, nwd, shift_tds);
|
||||
$(shift_tds).filter(`.${nwd}`).addClass("nwd");
|
||||
})
|
||||
}
|
||||
|
||||
workers[worker] = {
|
||||
"site": worker_td.attr("data-site"),
|
||||
"nwds": worker_td.attr("data-nwds"),
|
||||
"fte": worker_td.attr("data-fte_adj"),
|
||||
"end_date": worker_td.attr("data-end_date"),
|
||||
"shifts": shifts,
|
||||
"shift_types": shifts_unique,
|
||||
"weekends_worked": weekends_worked,
|
||||
"shift_counts": shift_counts,
|
||||
"days_lost": days_lost,
|
||||
//"shifts"
|
||||
}
|
||||
|
||||
sc = JSON.stringify(shift_counts);
|
||||
|
||||
worker_td.after(`<div class='worker-summary auto-generated'><span>Total shifts: ${total_shifts}, </span><span>Weekends: ${weekends_worked}, </span></div>`)
|
||||
|
||||
})
|
||||
|
||||
sites = {};
|
||||
sites_days_lost = {}
|
||||
for(const worker in workers) {
|
||||
//console.log(worker, workers[worker]);
|
||||
w = workers[worker];
|
||||
if(w.fte == 100) {
|
||||
//console.log(w.shift_types);
|
||||
for(let shift of w.shift_types) {
|
||||
//console.log(shift);
|
||||
if(!(w.site in sites)) {
|
||||
sites[w.site] = {};
|
||||
|
||||
}
|
||||
if(!(shift in sites[w.site])) {
|
||||
sites[w.site][shift] = [w.shift_counts[shift]];
|
||||
} else {
|
||||
sites[w.site][shift].push(w.shift_counts[shift]);
|
||||
}
|
||||
}
|
||||
if(!(w.site in sites_days_lost)) {
|
||||
sites_days_lost[w.site] = [w.days_lost];
|
||||
|
||||
} else {
|
||||
sites_days_lost[w.site].push(w.days_lost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate site shift summaries
|
||||
// $(table).append("<div class='site-summary auto-generated'>Average shifts for a 100% FTE trainee</div>");
|
||||
|
||||
// for(const site in sites) {
|
||||
// $(table).find(".site-summary").append(`<div class='site site-${site}'><span class='site-name'>${site}</span></div>`);
|
||||
|
||||
// for(const shift in sites[site]) {
|
||||
// average = mean(sites[site][shift])
|
||||
// $(table).find(`.site-${site}`).append(`<div>${shift}: ${average.toFixed(2)}</div>`);
|
||||
// }
|
||||
|
||||
// days_lost_avg = mean(sites_days_lost[site]);
|
||||
// $(table).find(`.site-${site}`).append(`<div class='training-days-lost'>Training days lost: ${days_lost_avg.toFixed(2)}</div>`);
|
||||
// }
|
||||
|
||||
summary_button = $("<button class='auto-generated'>Toggle Summary</button>").click(() => {
|
||||
$(table).find(".rota-day").toggleClass("hidden");
|
||||
$(table).find(".worker-summary").toggle();
|
||||
|
||||
});
|
||||
|
||||
|
||||
$(table).before(summary_button);
|
||||
|
||||
|
||||
|
||||
});
|
||||
return global_shifts
|
||||
}
|
||||
|
||||
shifts = generateExtra();
|
||||
|
||||
oshifts = Array.from(shifts).sort()
|
||||
|
||||
console.log(shifts)
|
||||
|
||||
$("body").append("<div><table class='tsummary'></table></div>")
|
||||
|
||||
$("table.tsummary").append("<tr class='header'></tr>");
|
||||
h = $("table.tsummary tr.header");
|
||||
h.append(`<th>Worker</th>`);
|
||||
h.append(`<th>Site</th>`);
|
||||
h.append(`<th>FTE</th>`);
|
||||
h.append(`<th>FTE (adj)</th>`);
|
||||
|
||||
|
||||
oshifts.forEach((s) => {
|
||||
h.append(`<th>${s}</th>`);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
$(".table-div .worker-row .worker").each((n, tr) => {
|
||||
//worker_td = $(tr).find("td").first();
|
||||
console.log(tr);
|
||||
jtr = $(tr);
|
||||
row = $("<tr></tr>");
|
||||
row.append(`<td>${jtr.data("worker")}</td>`)
|
||||
row.append(`<td>${jtr.data("site")}</td>`)
|
||||
row.append(`<td>${jtr.data("fte")}</td>`)
|
||||
row.append(`<td>${parseFloat(jtr.data("fte_adj")).toFixed(2)}</td>`)
|
||||
|
||||
console.log(jtr.data("shift-counts"))
|
||||
shift_counts = jtr.data("shift-counts")
|
||||
shift_targets = jtr.data("worker-targets")
|
||||
|
||||
oshifts.forEach((s) => {
|
||||
console.log(s)
|
||||
if(s in shift_targets && shift_targets[s] > 0) {
|
||||
if(s in shift_counts) {
|
||||
c = shift_counts[s];
|
||||
} else {
|
||||
c = 0;
|
||||
}
|
||||
row.append(`<td>${c} (${shift_targets[s].toFixed(2)})</td>`)
|
||||
|
||||
} else {
|
||||
row.append(`<td>-</td>`)
|
||||
}
|
||||
})
|
||||
console.log(shift_counts);
|
||||
|
||||
$("table.tsummary").append(row)
|
||||
})
|
||||
|
||||
//$("table.summary")
|
||||
|
||||
// $("body").prepend("<div id='global-settings'>Settings:<br />Customise training days lost per shift (you need to press recalculate for changes to take effect)<form></form></div>")
|
||||
|
||||
// shifts.forEach((s) => {
|
||||
|
||||
// disable = false;
|
||||
// if(s.includes("twilight")) {
|
||||
// value = 0.5;
|
||||
// } else if(s == "night_weekend") {
|
||||
// value = 4 / 3;
|
||||
// } else if(s == "night_weekday") {
|
||||
// value = 5 / 4;
|
||||
// } else {
|
||||
// disable = true;
|
||||
// value = 0;
|
||||
// }
|
||||
|
||||
// if(!disable) {
|
||||
// $("#global-settings form").append(`<span class='tdl-settings'><label for="${s}-tdl">${s}</label><input type="number" id="${s}-tdl" name="${s}-tdl" value='${value}'></span>`);
|
||||
// }
|
||||
|
||||
|
||||
// })
|
||||
// $("#global-settings").append($("<button >Recalculate</button>").click(() => {
|
||||
// generateExtra();
|
||||
// }))
|
||||
|
||||
// generateExtra();
|
||||
|
||||
$(".table-div + pre").each((n, pre) => {
|
||||
|
||||
$(pre).before($("<button class='shift-breakdown-button'>Show shift breakdown</button>").click(() => {
|
||||
$(pre).toggle();
|
||||
}));
|
||||
|
||||
})
|
||||
@@ -2,11 +2,15 @@ import datetime
|
||||
import itertools
|
||||
from typing import List, Tuple
|
||||
|
||||
import datetime
|
||||
|
||||
from pyomo.environ import *
|
||||
from pyomo.opt import SolverFactory
|
||||
|
||||
from workers import Worker
|
||||
|
||||
import json
|
||||
|
||||
import csv
|
||||
|
||||
from collections import defaultdict
|
||||
@@ -89,6 +93,9 @@ class RotaBuilder(object):
|
||||
ltft_balance_offset=4,
|
||||
max_night_frequency=2,
|
||||
max_weekend_frequency=2):
|
||||
|
||||
print("Start time: {}".format(datetime.datetime.now()))
|
||||
|
||||
self.shifts = [] # type List[SingleShift]
|
||||
|
||||
self.days = days
|
||||
@@ -98,9 +105,8 @@ class RotaBuilder(object):
|
||||
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()]))
|
||||
raise ValueError("Start date {} must be a Mon (not a {})".format(
|
||||
start_date.isoformat(), days[start_date.weekday()]))
|
||||
|
||||
self.start_date = start_date
|
||||
self.rota_days_length = len(self.weeks) * 7
|
||||
@@ -128,16 +134,31 @@ class RotaBuilder(object):
|
||||
self.constraint_options = {
|
||||
"limit_to_1_st1_on_nights": True,
|
||||
"ensure_1_st4_plus_on_nights": True,
|
||||
"ensure_derriford_reg_for_nights": True,
|
||||
"balance_nights": True,
|
||||
"constrain_time_off_after_nights": True,
|
||||
"balance_nights_across_sites": True,
|
||||
"balance_bank_holidays": True,
|
||||
"balance_blocks": True,
|
||||
"balance_shifts": True,
|
||||
"balance_weekends": True,
|
||||
"prevent_monday_after_full_weekends": False,
|
||||
"prevent_fridays_before_full_weekends": True,
|
||||
"max_weekends": False,
|
||||
"prevent_monday_after_full_weekends": [],
|
||||
"prevent_monday_and_tuesday_after_full_weekends": [],
|
||||
"prevent_fridays_before_full_weekends": [],
|
||||
"prevent_thursdays_before_full_weekends": [],
|
||||
"avoid_st1_first_month": False,
|
||||
}
|
||||
|
||||
# Generate a map for week, day combinations to a given date
|
||||
self.week_day_date_map = {}
|
||||
|
||||
n = 0
|
||||
for week, day in self.get_week_day_combinations():
|
||||
d = self.start_date + datetime.timedelta(n)
|
||||
self.week_day_date_map[(week, day)] = d
|
||||
n = n + 1
|
||||
|
||||
def build_model(self):
|
||||
# Initialize model
|
||||
self.model = ConcreteModel()
|
||||
@@ -191,6 +212,26 @@ class RotaBuilder(object):
|
||||
initialize=0,
|
||||
)
|
||||
|
||||
if self.constraint_options["balance_bank_holidays"]:
|
||||
# Bank holidays
|
||||
self.model.bank_holiday_count = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
within=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
self.model.bank_holiday_count_w = Var(
|
||||
((worker.id) for worker in self.workers),
|
||||
within=NonNegativeIntegers,
|
||||
initialize=0,
|
||||
)
|
||||
# self.model.bank_holiday_count = Var(
|
||||
# ((worker.id, week, day) for worker in self.workers
|
||||
# for week, day in self.get_week_day_combinations() if self.week_day_date_map[(week, day)] in bank_holiday_map
|
||||
# ),
|
||||
# within=Binary,
|
||||
# initialize=0,
|
||||
# )
|
||||
|
||||
# Used to limit number of workers on night shift per site
|
||||
# if we force a binary it will in effect hard constrain to < 2
|
||||
# from a single site
|
||||
@@ -362,7 +403,6 @@ class RotaBuilder(object):
|
||||
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
|
||||
|
||||
@@ -478,7 +518,6 @@ class RotaBuilder(object):
|
||||
def nightShiftMinST4Rule(model, week, shift):
|
||||
single_workers = [w for w in self.workers if w.grade >= 4]
|
||||
if not single_workers:
|
||||
print(single_workers)
|
||||
return Constraint.Skip
|
||||
return sum(model.shift_week_worker_assigned[shift, week, w.id]
|
||||
for w in single_workers) >= 1
|
||||
@@ -493,6 +532,23 @@ class RotaBuilder(object):
|
||||
rule=nightShiftMinST4Rule,
|
||||
)
|
||||
|
||||
def nightShiftDerrifordRule(model, week, shift):
|
||||
derriford_workers = [w for w in self.workers if w.night_at_derriford >= 1]
|
||||
#if not derriford_workers:
|
||||
#return Constraint.Skip
|
||||
return sum(model.shift_week_worker_assigned[shift, week, w.id]
|
||||
for w in derriford_workers) >= 1
|
||||
|
||||
if self.constraint_options["ensure_derriford_reg_for_nights"]:
|
||||
self.model.night_shifts_derriford_rule = Constraint(
|
||||
[week for week in self.weeks],
|
||||
[
|
||||
shift.name
|
||||
for shift in self.get_shifts_with_constraint("night")
|
||||
],
|
||||
rule=nightShiftDerrifordRule,
|
||||
)
|
||||
|
||||
# # Count the number of workers from each site on each night shift
|
||||
# # As 1 or 0 is optimum we can simply subtract 1 from the number
|
||||
if self.constraint_options["balance_nights_across_sites"]:
|
||||
@@ -548,6 +604,14 @@ class RotaBuilder(object):
|
||||
# Most of our constraints apply per worker
|
||||
for worker in self.workers:
|
||||
|
||||
if self.constraint_options[
|
||||
"avoid_st1_first_month"] and worker.grade == 2:
|
||||
# Avoid ST1s on the first month
|
||||
self.model.constraints.add(0 == sum(
|
||||
self.model.works[worker.id, week, day, shift] for week,
|
||||
day, shift in self.get_all_shiftname_combinations()
|
||||
if week in (1, 2, 3, 4) and shift in ("night_weekday", "night_weekend")))
|
||||
|
||||
# Count number of weekends an worker works
|
||||
if self.constraint_options["balance_weekends"]:
|
||||
self.model.constraints.add(
|
||||
@@ -555,6 +619,11 @@ class RotaBuilder(object):
|
||||
self.model.works_weekend[worker.id, week]
|
||||
for week in self.weeks))
|
||||
|
||||
if self.constraint_options["max_weekends"]:
|
||||
self.model.constraints.add(
|
||||
self.constraint_options["max_weekends"] >=
|
||||
self.model.worker_weekend_count[worker.id])
|
||||
|
||||
# Balance shifts within required limits (set by balance_offset)
|
||||
# If balance_offset is too restrictive (for the rota length)
|
||||
# no solution will be possible
|
||||
@@ -606,13 +675,6 @@ class RotaBuilder(object):
|
||||
self.model.works[worker.id, week, day, shift.name]
|
||||
for week, day in self.get_week_day_combinations()))
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
self.model.constraints.add(
|
||||
self.model.night_shift_count[worker.id] == sum(
|
||||
self.model.works[worker.id, week, day, shift.name]
|
||||
for week, day in self.get_week_day_combinations()
|
||||
for shift in self.get_shifts_with_constraint("night")))
|
||||
|
||||
# Define shift_count_t1 and shift_count_t2 constraints for the object
|
||||
# Thus bypassing the need for a quadratic solver
|
||||
# t1-t2 is the target
|
||||
@@ -628,7 +690,40 @@ class RotaBuilder(object):
|
||||
for shift in self.get_shifts()))
|
||||
#if "night" not in shift.constraints))
|
||||
|
||||
if self.constraint_options["balance_bank_holidays"]:
|
||||
self.model.constraints.add(
|
||||
self.model.bank_holiday_count[worker.id] == sum(
|
||||
self.model.works[worker.id, week, day, shift] for week,
|
||||
day, shift in self.get_all_shiftname_combinations()
|
||||
if self.week_day_date_map[(week,
|
||||
day)] in bank_holiday_map))
|
||||
|
||||
xU = len(self.get_bank_holiday_week_days()) + 1
|
||||
xL = 1
|
||||
self.model.constraints.add(
|
||||
inequality(
|
||||
xL,
|
||||
self.model.bank_holiday_count[worker.id] + 1,
|
||||
xU,
|
||||
))
|
||||
|
||||
self.model.constraints.add(
|
||||
self.model.bank_holiday_count_w[worker.id] >= xL *
|
||||
(self.model.bank_holiday_count[worker.id] + 1) * 2 -
|
||||
xL * xL)
|
||||
|
||||
self.model.constraints.add(
|
||||
self.model.bank_holiday_count_w[worker.id] >= xU *
|
||||
(self.model.bank_holiday_count[worker.id] + 1) * 2 -
|
||||
xU * xU)
|
||||
|
||||
if self.constraint_options["balance_nights"]:
|
||||
self.model.constraints.add(
|
||||
self.model.night_shift_count[worker.id] == sum(
|
||||
self.model.works[worker.id, week, day, shift.name]
|
||||
for week, day in self.get_week_day_combinations()
|
||||
for shift in self.get_shifts_with_constraint("night")))
|
||||
|
||||
night_shift_target_number = sum(
|
||||
worker.shift_target_number[shift.name]
|
||||
for shift in self.get_shifts_with_constraint("night"))
|
||||
@@ -676,12 +771,14 @@ class RotaBuilder(object):
|
||||
# self.model.night_shift_count_w[worker.id] >= 0)
|
||||
|
||||
# We use a similar method to balance the number of weekends worked
|
||||
# I'm not entirely sure how split shifts will affect this
|
||||
# This works as long as weekend shifts are assigned as blocks!
|
||||
if self.constraint_options["balance_weekends"]:
|
||||
weekend_shift_target_number = sum(
|
||||
worker.shift_target_number[shift.name]
|
||||
for shift in self.get_shifts()
|
||||
if not set(("Sat", "Sun")).isdisjoint(shift.shift_days))
|
||||
worker.shift_target_number[shift.name] /
|
||||
len(shift.shift_days) for shift in self.get_shifts()
|
||||
if "Sat" in shift.shift_days or "Sun" in shift.shift_days)
|
||||
|
||||
worker.weekend_shift_target_number = weekend_shift_target_number
|
||||
|
||||
# min_shifts = weekend_shift_target_number - 20
|
||||
# max_shifts = weekend_shift_target_number + 20
|
||||
@@ -699,7 +796,8 @@ class RotaBuilder(object):
|
||||
self.model.worker_weekend_count[worker.id] -
|
||||
weekend_shift_target_number)
|
||||
|
||||
xU = 10
|
||||
xU = self.constraint_options["max_weekends"]
|
||||
|
||||
xL = 1
|
||||
self.model.constraints.add(
|
||||
inequality(
|
||||
@@ -750,7 +848,13 @@ class RotaBuilder(object):
|
||||
for week in week_blocks))
|
||||
|
||||
for week in self.weeks:
|
||||
if self.constraint_options[
|
||||
for shift in self.get_shifts_with_constraint(
|
||||
"max_2_consecutive_shifts_per_week"):
|
||||
self.model.constraints.add(2 >= sum(
|
||||
self.model.works[worker.id, week, day, shift.name]
|
||||
for day in self.days))
|
||||
|
||||
if worker.site in self.constraint_options[
|
||||
"prevent_monday_after_full_weekends"]:
|
||||
# Ignore last week
|
||||
if week + 1 in self.weeks:
|
||||
@@ -764,21 +868,53 @@ class RotaBuilder(object):
|
||||
sum(self.model.works[worker.id, week + 1, "Mon",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()))
|
||||
if self.constraint_options[
|
||||
if worker.site in self.constraint_options[
|
||||
"prevent_monday_and_tuesday_after_full_weekends"]:
|
||||
# Ignore last week
|
||||
if week + 1 in self.weeks:
|
||||
self.model.constraints.add(
|
||||
2 >= sum(self.model.works[worker.id, week, "Sat",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()) +
|
||||
sum(self.model.works[worker.id, week, "Sun",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()) +
|
||||
sum(self.model.works[worker.id, week + 1, "Mon",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()) +
|
||||
sum(self.model.works[worker.id, week + 1, "Tue",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()))
|
||||
if worker.site in self.constraint_options[
|
||||
"prevent_fridays_before_full_weekends"]:
|
||||
self.model.constraints.add(
|
||||
2 >= sum(self.model.works[worker.id, week, "Sat",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()
|
||||
if shift.name not in ("night_weekend")) +
|
||||
if "night" not in shift.constraints) +
|
||||
sum(self.model.works[worker.id, week, "Sun",
|
||||
shift.name]
|
||||
for shift in self.get_shifts() if shift.name not in
|
||||
("night_weekend")) +
|
||||
for shift in self.get_shifts()
|
||||
if "night" not in shift.constraints) +
|
||||
sum(self.model.works[worker.id, week, "Fri",
|
||||
shift.name]
|
||||
for shift in self.get_shifts() if shift.name not in
|
||||
("night_weekend")))
|
||||
for shift in self.get_shifts()
|
||||
if "night" not in shift.constraints))
|
||||
if worker.site in self.constraint_options[
|
||||
"prevent_thursdays_before_full_weekends"]:
|
||||
self.model.constraints.add(
|
||||
2 >= sum(self.model.works[worker.id, week, "Sat",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()
|
||||
if "night" not in shift.constraints) +
|
||||
sum(self.model.works[worker.id, week, "Sun",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()
|
||||
if "night" not in shift.constraints) +
|
||||
sum(self.model.works[worker.id, week, "Thu",
|
||||
shift.name]
|
||||
for shift in self.get_shifts()
|
||||
if "night" not in shift.constraints))
|
||||
|
||||
# # model.weekend_count stores the number of weekend shifts that a worker is assigned to wor
|
||||
# # this is used (by the objective) to balance the total number of weekend shifts worked
|
||||
@@ -959,6 +1095,17 @@ class RotaBuilder(object):
|
||||
else:
|
||||
night_shift_balancing = 0
|
||||
|
||||
if self.constraint_options["balance_bank_holidays"]:
|
||||
bank_holiday_balance_modifier_constant = 1000
|
||||
bank_holiday_balancing = sum(
|
||||
bank_holiday_balance_modifier_constant *
|
||||
(self.model.bank_holiday_count_w[(worker.id)] - 1)
|
||||
# (self.model.night_shift_count_t1[(worker.id)] +
|
||||
# self.model.night_shift_count_t2[(worker.id)])
|
||||
for worker in self.workers)
|
||||
else:
|
||||
bank_holiday_balancing = 0
|
||||
|
||||
if self.constraint_options["balance_weekends"]:
|
||||
weekend_balance_modifier_constant = 5
|
||||
weekend_shift_balancing = sum(
|
||||
@@ -977,7 +1124,7 @@ class RotaBuilder(object):
|
||||
for week, day, shift in self.get_all_shiftname_combinations())
|
||||
|
||||
# Work requsets
|
||||
work_request_constant = 1000
|
||||
work_request_constant = 10000 # Needs to be very big to override loss from bank holiday requests
|
||||
work_requests = sum(
|
||||
self.model.work_requests[worker.id, week, day, shift] *
|
||||
self.model.works[worker.id, week, day, shift] *
|
||||
@@ -1006,9 +1153,10 @@ class RotaBuilder(object):
|
||||
blocks_balancing = 0
|
||||
|
||||
#return weekend_shift_balancing + blocks_balancing
|
||||
#return bank_holiday_balancing
|
||||
#return shift_balancing + preferences + blocks_balancing
|
||||
#return shift_balancing + preferences + nights_site_balancing + blocks_balancing
|
||||
return shift_balancing + night_shift_balancing + preferences + nights_site_balancing + blocks_balancing - work_requests
|
||||
return weekend_shift_balancing + shift_balancing + night_shift_balancing + preferences + nights_site_balancing + bank_holiday_balancing + blocks_balancing - work_requests
|
||||
|
||||
# add objective function to the model. rule (pass function) or expr (pass expression directly)
|
||||
self.model.obj = Objective(rule=obj_rule, sense=minimize)
|
||||
@@ -1273,6 +1421,10 @@ class RotaBuilder(object):
|
||||
t = "\n".join(l)
|
||||
return "Full time equivalent trainees by site:\n{}".format(t)
|
||||
|
||||
def get_bank_holiday_week_days(self):
|
||||
return ([(week, day) for week, day in self.get_week_day_combinations()
|
||||
if self.week_day_date_map[(week, day)] in bank_holiday_map])
|
||||
|
||||
|
||||
class RotaResults(object):
|
||||
def __init__(self, rota, results):
|
||||
@@ -1431,13 +1583,13 @@ class RotaResults(object):
|
||||
else:
|
||||
nwds = None
|
||||
|
||||
w = [
|
||||
"<td title='Site: {}' class='worker {}' data-nwds='{}' data-site='{}' data-worker='{}' data-fte='{}' data-fte_adj='{}' data-end_date='{}'>{} ({}) [{}]</td>"
|
||||
.format(worker.site, worker.site, nwds, worker.site,
|
||||
worker.name, worker.fte, worker.fte_adj,
|
||||
worker.end_date, worker.name, worker.grade, worker.fte)
|
||||
]
|
||||
worker_targets = json.dumps(worker.shift_target_number)
|
||||
|
||||
tds = []
|
||||
|
||||
n = 0
|
||||
|
||||
shift_tds = []
|
||||
for week, day in self.rota.get_week_day_combinations():
|
||||
d = self.rota.start_date + datetime.timedelta(n)
|
||||
|
||||
@@ -1445,19 +1597,21 @@ class RotaResults(object):
|
||||
a = "-"
|
||||
shift_name = ""
|
||||
|
||||
title = "{} ({})".format(shift_name, d)
|
||||
for shift in self.rota.get_shift_names_by_day(day):
|
||||
if model.works[worker.id, week, day, shift].value == 1:
|
||||
shifts.append(shift)
|
||||
a = shift[0]
|
||||
shift_name = shift
|
||||
break
|
||||
title = "{} ({})".format(shift_name, d)
|
||||
css_class = day
|
||||
if model.available[worker.id, week, day] > 0:
|
||||
available = True
|
||||
unavailable_reason = ""
|
||||
else:
|
||||
available = False
|
||||
css_class = "unavailable"
|
||||
unavailable_reason = self.rota.unavailable_to_work
|
||||
|
||||
bank_holiday = ""
|
||||
if d in bank_holiday_map:
|
||||
@@ -1472,20 +1626,36 @@ class RotaResults(object):
|
||||
requests = " data-shift-request='{}'".format(
|
||||
self.rota.work_requests_map[(worker.id, week, day)])
|
||||
|
||||
w.append(
|
||||
shift_tds.append(
|
||||
"<td title='{}' class='rota-day {}' data-shift='{}' data-available='{}' data-date='{}' data-week='{}' data-day='{}'{}{}>{}</td>"
|
||||
.format(title, css_class, shift_name, available, d, week,
|
||||
day, requests, bank_holiday, a))
|
||||
|
||||
shift_count = ""
|
||||
shift_count_dict = {}
|
||||
for s in set(shifts):
|
||||
shift_count = shift_count + "{}: {}, ".format(
|
||||
s, shifts.count(s))
|
||||
c = shifts.count(s)
|
||||
shift_count_dict[s] = c
|
||||
shift_count = shift_count + "{}: {}, ".format(s, c)
|
||||
|
||||
worker_td = "<td title='Site: {}' class='worker {}' data-nwds='{}' data-site='{}' data-worker='{}' data-fte='{}' data-fte_adj='{}' data-end_date='{}' data-worker-targets='{}' data-shift-counts='{}' data-weekend-target='{}' data-night-at-derriford='{}'>{} ({}) [{}]</td>".format(
|
||||
worker.site, worker.site, nwds, worker.site, worker.name,
|
||||
worker.fte, worker.fte_adj, worker.end_date, worker_targets,
|
||||
json.dumps(shift_count_dict),
|
||||
worker.weekend_shift_target_number, worker.night_at_derriford, worker.name, worker.grade,
|
||||
worker.fte)
|
||||
|
||||
print(shift_count_dict)
|
||||
|
||||
shift_count = shift_count + "#weekends_worked: {}\\#".format(
|
||||
model.worker_weekend_count[worker.id].value)
|
||||
timetable.append("<tr class='worker-row'>{}</tr>".format(
|
||||
"".join(w)))
|
||||
|
||||
bank_holiday_count = model.bank_holiday_count[worker.id].value
|
||||
bank_holiday_count_w = model.bank_holiday_count_w[
|
||||
worker.id].value - 1
|
||||
print(worker.name, bank_holiday_count, bank_holiday_count_w)
|
||||
timetable.append("<tr class='worker-row'>{}{}</tr>".format(
|
||||
worker_td, "".join(shift_tds)))
|
||||
#timetable.append("<tr>{}</tr>".format("".join(w), shift_count))
|
||||
|
||||
# if show_prefs:
|
||||
|
||||
@@ -17,110 +17,90 @@ import operator
|
||||
|
||||
use_neos = False
|
||||
|
||||
weeks_to_rota = 8
|
||||
weeks_to_rota = 26
|
||||
|
||||
time_to_run = 1200
|
||||
allow = 500
|
||||
time_to_run = 152000
|
||||
allow = 2000
|
||||
|
||||
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
|
||||
|
||||
Rota = RotaBuilder(datetime.date(2020, 9, 7),
|
||||
weeks_to_rota=weeks_to_rota,
|
||||
balance_offset_modifier=8,
|
||||
balance_offset_modifier=1,
|
||||
max_weekend_frequency=2,
|
||||
max_night_frequency=2)
|
||||
|
||||
Rota.constraint_options["limit_to_1_st1_on_nights"] = False
|
||||
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = False
|
||||
Rota.constraint_options["balance_nights"] = False
|
||||
Rota.constraint_options["constrain_time_off_after_nights"] = False
|
||||
Rota.constraint_options["balance_nights_across_sites"] = False
|
||||
Rota.constraint_options["limit_to_1_st1_on_nights"] = True
|
||||
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = True
|
||||
Rota.constraint_options["balance_nights"] = True
|
||||
Rota.constraint_options["constrain_time_off_after_nights"] = True
|
||||
Rota.constraint_options["balance_nights_across_sites"] = True
|
||||
Rota.constraint_options["balance_blocks"] = True
|
||||
Rota.constraint_options["balance_weekends"] = True
|
||||
Rota.constraint_options["prevent_monday_after_full_weekends"] = False
|
||||
Rota.constraint_options["prevent_fridays_before_full_weekends"] = True
|
||||
Rota.constraint_options["avoid_st1_first_month"] = True
|
||||
Rota.constraint_options["max_weekends"] = 10
|
||||
Rota.constraint_options["prevent_monday_and_tuesday_after_full_weekends"] = [
|
||||
"truro", "torquay", "exeter"
|
||||
]
|
||||
Rota.constraint_options["prevent_monday_after_full_weekends"] = ["plymouth"]
|
||||
Rota.constraint_options["prevent_fridays_before_full_weekends"] = ["plymouth"]
|
||||
Rota.constraint_options["prevent_thursdays_before_full_weekends"] = [
|
||||
"truro", "torquay", "exeter"
|
||||
]
|
||||
|
||||
Rota.add_shifts(
|
||||
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||
# # SingleShift(("truro", ),
|
||||
# # "truro_twilight",
|
||||
# # 12.5,
|
||||
# # days[:5],
|
||||
# # assign_as_block=False),
|
||||
# #SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||
# #SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("exeter", ),
|
||||
# "weekend_exeter",
|
||||
# 12.5,
|
||||
# days[5:],
|
||||
# assign_as_block=False),
|
||||
# SingleShift(("truro", ),
|
||||
# "weekend_truro",
|
||||
# 12.5,
|
||||
# days[5:],
|
||||
# force_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_weekday",
|
||||
# 12.25,
|
||||
# days[:4],
|
||||
# balance_offset=2,
|
||||
# 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=2,
|
||||
# balance_weighting=1,
|
||||
# workers_required=3,
|
||||
# force_as_block=False,
|
||||
# rota_on_nwds=True,
|
||||
# constraints=["night"]),
|
||||
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
||||
# SingleShift(("exeter", ),
|
||||
# "weekend_exeter",
|
||||
# 12.5,
|
||||
# days[5:],
|
||||
# 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(("exeter", ), "exeter_twilight", 12.5, days[:4]),
|
||||
SingleShift(("truro", ), "truro_twilight", 12.5, days[:4]),
|
||||
SingleShift(("torquay", ),
|
||||
"torquay_twilight",
|
||||
12.5,
|
||||
days[:4],
|
||||
assign_as_block=True),
|
||||
SingleShift(("plymouth", ),
|
||||
"plymouth_twilight",
|
||||
12.5,
|
||||
days[:5],
|
||||
workers_required=1,
|
||||
constraints=["max_2_consecutive_shifts_per_week"]),
|
||||
SingleShift(("exeter", ),
|
||||
"weekend_exeter",
|
||||
12.5,
|
||||
days[4:],
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True),
|
||||
SingleShift(("truro", ),
|
||||
"weekend_truro",
|
||||
12.5,
|
||||
days[4:],
|
||||
balance_offset=1,
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True),
|
||||
SingleShift(("torquay", ),
|
||||
"weekend_torquay",
|
||||
12.5,
|
||||
days[4:],
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True),
|
||||
SingleShift(("plymouth", ),
|
||||
"weekend_plymouth1",
|
||||
8,
|
||||
days[5:],
|
||||
workers_required=1,
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True),
|
||||
SingleShift(("plymouth", ),
|
||||
"weekend_plymouth2",
|
||||
8,
|
||||
days[5:],
|
||||
workers_required=1,
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True),
|
||||
SingleShift(
|
||||
(sites),
|
||||
"night_weekday",
|
||||
12.25,
|
||||
days[:4],
|
||||
balance_offset=2,
|
||||
balance_offset=4,
|
||||
balance_weighting=1,
|
||||
#hard_constrain_shift=False,
|
||||
workers_required=3,
|
||||
@@ -132,7 +112,7 @@ Rota.add_shifts(
|
||||
"night_weekend",
|
||||
12.25,
|
||||
days[4:],
|
||||
balance_offset=2,
|
||||
balance_offset=4,
|
||||
balance_weighting=1,
|
||||
#hard_constrain_shift=False,
|
||||
workers_required=3,
|
||||
@@ -143,6 +123,14 @@ Rota.add_shifts(
|
||||
|
||||
use_test_workers = False
|
||||
|
||||
load_leave = True
|
||||
Rota.build_shifts_and_workers()
|
||||
|
||||
if load_leave:
|
||||
import leave
|
||||
|
||||
leave, requests, site_prefs = leave.load_leave(Rota)
|
||||
|
||||
# Import trainee data
|
||||
if use_test_workers:
|
||||
# Rota.add_workers([
|
||||
@@ -203,61 +191,82 @@ else:
|
||||
n = 0
|
||||
for row in reader:
|
||||
n = n + 1
|
||||
#print(row)
|
||||
|
||||
name, site, grade, fte, nwd, end_date, oop, extra = row
|
||||
|
||||
# Ignore trainees if fte == 0 (what are they doing here anyway)
|
||||
if int(fte) < 1:
|
||||
continue
|
||||
|
||||
nwds = nwd.split(",").title() if nwd else None
|
||||
nwds = nwd.split("/") if nwd else None
|
||||
end_date = end_date if end_date else None
|
||||
oop = oop.split("-") if oop else None
|
||||
|
||||
leave_requests = []
|
||||
if load_leave:
|
||||
if name not in leave:
|
||||
raise ValueError(
|
||||
"{} not found it leave requests".format(name))
|
||||
|
||||
leave_requests = [i[0] for i in leave[name] if i[0] != ""]
|
||||
work_requests = [i for i in requests[name] if i[0] != ""]
|
||||
site_pref = int(site_prefs[name][0])
|
||||
#print(nwds, end_date, oop)
|
||||
# Rota.add_worker(
|
||||
# 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
|
||||
# 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/12/25",
|
||||
# "exeter_twilight"), )))
|
||||
# continue
|
||||
Rota.add_worker(
|
||||
Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte),
|
||||
nwds, end_date, oop))
|
||||
Worker(Rota,
|
||||
n,
|
||||
name,
|
||||
site.lower(),
|
||||
int(grade[2]),
|
||||
int(fte),
|
||||
nwds,
|
||||
end_date,
|
||||
oop,
|
||||
not_available_to_work=leave_requests,
|
||||
work_requests=work_requests,
|
||||
night_at_derriford=site_pref))
|
||||
|
||||
print(0)
|
||||
print("Building model")
|
||||
Rota.build_shifts_and_workers()
|
||||
|
||||
Rota.build_model()
|
||||
|
||||
#print(Rota.get_worker_details())
|
||||
print(1)
|
||||
print("Setting up solver")
|
||||
opt = SolverFactory('cbc') # choose a solver
|
||||
#opt = SolverFactory('ipopt') # choose a solver
|
||||
|
||||
print(2)
|
||||
print("Solving")
|
||||
if use_neos:
|
||||
solver_manager = SolverManagerFactory('neos') # Solve in neos server
|
||||
results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log")
|
||||
@@ -268,6 +277,7 @@ else:
|
||||
options={
|
||||
"seconds": time_to_run,
|
||||
"allow": allow,
|
||||
"threads": 4,
|
||||
},
|
||||
logfile="test.log"
|
||||
) # solve the model with the, options="seconds=60" selected solver
|
||||
@@ -284,8 +294,8 @@ worker_timetable = ResultsHolder.get_worker_timetable()
|
||||
worker_timetable_brief = ResultsHolder.get_worker_timetable_brief(
|
||||
show_prefs=False, show_unavailable=False)
|
||||
|
||||
print(worker_timetable_brief)
|
||||
print(Rota.get_workers_total_fte())
|
||||
#print(worker_timetable_brief)
|
||||
#print(Rota.get_workers_total_fte())
|
||||
|
||||
ResultsHolder.get_weekend_details()
|
||||
#ResultsHolder.get_night_details()
|
||||
@@ -293,6 +303,8 @@ ResultsHolder.get_weekend_details()
|
||||
with open("test.html", "w") as f:
|
||||
f.write(ResultsHolder.get_worker_timetable_html(True))
|
||||
|
||||
ResultsHolder.export_rota_to_csv()
|
||||
|
||||
# for i in ResultsHolder.rota.model.blocks_worker_shift_count:
|
||||
# n = ResultsHolder.rota.model.blocks_worker_shift_count[i].value
|
||||
|
||||
|
||||
+78
-17
@@ -7,20 +7,27 @@ table {
|
||||
/* margin-top: 50px; */
|
||||
padding-top: 100px;
|
||||
text-align:center;
|
||||
table-layout : fixed;
|
||||
/* table-layout : fixed; */
|
||||
/* width:150px */
|
||||
width: 200%;
|
||||
overflow-x: auto;
|
||||
/* width: 200%; */
|
||||
/* overflow-x: auto;
|
||||
display: block;
|
||||
position: relative;
|
||||
position: relative; */
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
|
||||
}
|
||||
|
||||
#table-div {
|
||||
overflow-x: auto;
|
||||
.table-div {
|
||||
/* overflow-x: auto; */
|
||||
width: 80%;
|
||||
overflow-x: scroll;
|
||||
margin-left: 200px;
|
||||
overflow-y: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
th {
|
||||
.table-div th {
|
||||
text-align:center;
|
||||
white-space:nowrap;
|
||||
transform: rotate(90deg) translateX(-100px);
|
||||
@@ -28,11 +35,11 @@ th {
|
||||
top:0;
|
||||
|
||||
}
|
||||
th p {
|
||||
.table-div th p {
|
||||
margin:0 -100% ;
|
||||
display:inline-block;
|
||||
}
|
||||
th p:before{
|
||||
.table-div th p:before{
|
||||
content:'';
|
||||
width:0;
|
||||
padding-top:110%;/* takes width as reference, + 10% for faking some extra padding */
|
||||
@@ -40,20 +47,20 @@ th p:before{
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
td, th {
|
||||
.table-div td, th {
|
||||
max-width: 10px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
tr {
|
||||
.table-div tr {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
.table-div tr:hover {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
tr:hover .unavailable{
|
||||
.table-div tr:hover .unavailable{
|
||||
|
||||
background-color: lightgray;
|
||||
}
|
||||
@@ -70,15 +77,14 @@ tr:hover .unavailable{
|
||||
}
|
||||
|
||||
.bank-holiday {
|
||||
border: solid 1px blue;
|
||||
border-bottom: dotted 1px blue;
|
||||
|
||||
}
|
||||
|
||||
th.worker {
|
||||
/* th.worker {
|
||||
max-width: 100%;
|
||||
width: 200px;
|
||||
/* width: 100px; */
|
||||
}
|
||||
} */
|
||||
|
||||
.plymouth {
|
||||
color: #9169FF;
|
||||
@@ -102,6 +108,8 @@ th.worker {
|
||||
.worker-summary {
|
||||
position: absolute;
|
||||
display: none;
|
||||
overflow-x: visible;
|
||||
width: 100%;
|
||||
}
|
||||
.site-summary {
|
||||
display: inline-flex;
|
||||
@@ -125,3 +133,56 @@ th.worker {
|
||||
.training-days-lost {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.tdl-settings {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.table-div + pre {
|
||||
display: none;
|
||||
}
|
||||
.shift-breakdown-button + pre {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shift-breakdown-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nwd {
|
||||
color: brown;
|
||||
}
|
||||
|
||||
.th {
|
||||
|
||||
}
|
||||
|
||||
.table-div th.worker, td.worker {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
max-width: 200px;
|
||||
left: 0;
|
||||
top: auto;
|
||||
border-top-width: 1px;
|
||||
/*only relevant for first row*/
|
||||
margin-top: -1px;
|
||||
/*compensate for top border*/
|
||||
}
|
||||
|
||||
.hidden {
|
||||
opacity: 0%;
|
||||
}
|
||||
|
||||
.tsummary {
|
||||
max-width: auto;
|
||||
}
|
||||
|
||||
.header th {
|
||||
max-width: 200px;
|
||||
|
||||
}
|
||||
|
||||
[data-night-at-derriford="1"] {
|
||||
background-color: yellow;
|
||||
/* Attribute has this exact value */
|
||||
}
|
||||
+37
-17
@@ -5,18 +5,22 @@ from collections import defaultdict
|
||||
|
||||
|
||||
class Worker:
|
||||
def __init__(self,
|
||||
Rota,
|
||||
id,
|
||||
name,
|
||||
site,
|
||||
grade,
|
||||
fte=100,
|
||||
nwd=None,
|
||||
end_date=None,
|
||||
oop=None,
|
||||
pref_not_to_work=None,
|
||||
work_requests=None,):
|
||||
def __init__(
|
||||
self,
|
||||
Rota,
|
||||
id,
|
||||
name,
|
||||
site,
|
||||
grade,
|
||||
fte=100,
|
||||
nwd=None,
|
||||
end_date=None,
|
||||
oop=None,
|
||||
not_available_to_work=None,
|
||||
pref_not_to_work=None,
|
||||
work_requests=None,
|
||||
night_at_derriford=0,
|
||||
):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.site = site
|
||||
@@ -26,6 +30,7 @@ class Worker:
|
||||
self.fte = fte
|
||||
self.nwd = nwd
|
||||
self.proportion_rota_to_work = 1
|
||||
self.night_at_derriford = night_at_derriford
|
||||
|
||||
self.shift_target_number = defaultdict(int)
|
||||
|
||||
@@ -54,7 +59,6 @@ class Worker:
|
||||
end_oop_date = datetime.datetime.strptime(end_oop,
|
||||
"%Y/%m/%d").date()
|
||||
|
||||
print(start_oop_date, end_oop_date)
|
||||
if end_oop_date > Rota.rota_end_date:
|
||||
end_oop_date = Rota.rota_end_date
|
||||
|
||||
@@ -74,27 +78,43 @@ class Worker:
|
||||
if pref_not_to_work is not None:
|
||||
# loop throught dates converting to week / day combination
|
||||
for date in pref_not_to_work:
|
||||
days_from_start = (datetime.datetime.strptime(date, "%Y/%m/%d").date() - Rota.start_date).days
|
||||
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)]
|
||||
# Weight the value to take into account the number of preferences
|
||||
# 1 is added to the total number of requests to ensure they do not outweight
|
||||
# a single (or fewer) request(s)
|
||||
Rota.pref_not_to_work[(self.id, week, day)] = 1 / (len(pref_not_to_work) + 1)
|
||||
Rota.pref_not_to_work[(self.id, week,
|
||||
day)] = 1 / (len(pref_not_to_work) + 1)
|
||||
|
||||
if not_available_to_work is not None:
|
||||
# loop throught dates converting to week / day combination
|
||||
for date in not_available_to_work:
|
||||
days_from_start = (
|
||||
datetime.datetime.strptime(date, "%d/%m/%Y").date() -
|
||||
Rota.start_date).days
|
||||
week = days_from_start // 7 + 1
|
||||
day = Rota.days[(days_from_start % 7)]
|
||||
Rota.unavailable_to_work.add((self.id, week, day))
|
||||
|
||||
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
|
||||
days_from_start = (
|
||||
datetime.datetime.strptime(date, "%d/%m/%Y").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
|
||||
|
||||
# We had to adjust the full time equivalent for people who CCT / leave the rota early
|
||||
self.fte_adj = self.fte * self.proportion_rota_to_work
|
||||
|
||||
# print(self.name, self.nwd)
|
||||
|
||||
def __lt__(self, other):
|
||||
return (self.site, self.grade, self.fte_adj, self.name) < (
|
||||
other.site,
|
||||
|
||||
Reference in New Issue
Block a user