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
|
use_neos = False
|
||||||
|
|
||||||
weeks_to_rota = 26
|
weeks_to_rota = 52
|
||||||
|
|
||||||
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
|
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
|
||||||
|
|
||||||
@@ -29,13 +29,13 @@ Rota = RotaBuilder(datetime.date(2020, 9, 7),
|
|||||||
|
|
||||||
rota_collections = {
|
rota_collections = {
|
||||||
"current": deepcopy(Rota),
|
"current": deepcopy(Rota),
|
||||||
"extended proc nights only": deepcopy(Rota),
|
# "extended proc nights only": deepcopy(Rota),
|
||||||
"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 twighlights3 + normal weekends": deepcopy(Rota),
|
# "nights + proc twighlights3 + normal weekends": deepcopy(Rota),
|
||||||
"nights + proc twighlights4 + 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),
|
||||||
}
|
}
|
||||||
|
|
||||||
rota_collections['current'].add_shifts(
|
rota_collections['current'].add_shifts(
|
||||||
@@ -86,249 +86,257 @@ rota_collections['current'].add_shifts(
|
|||||||
constraints=["night"]),
|
constraints=["night"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
rota_collections['extended proc nights only'].add_shifts(
|
# 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", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekday",
|
# "night_weekday",
|
||||||
13,
|
# 13,
|
||||||
days[:4],
|
# days[:4],
|
||||||
balance_offset=5,
|
# balance_offset=5,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekend",
|
# "night_weekend",
|
||||||
13,
|
# 13,
|
||||||
days[4:],
|
# days[4:],
|
||||||
balance_offset=4,
|
# balance_offset=4,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
)
|
# )
|
||||||
|
|
||||||
rota_collections['no weekends'].add_shifts(
|
# rota_collections['no weekends'].add_shifts(
|
||||||
SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||||
SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
||||||
SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||||
SingleShift(("plymouth", ), "plymouth_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", 12.5, days, balance_weighting=1, workers_required=3, rota_on_nwds=True),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekday",
|
# "night_weekday",
|
||||||
13,
|
# 13,
|
||||||
days[:4],
|
# days[:4],
|
||||||
balance_offset=5,
|
# balance_offset=5,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekend",
|
# "night_weekend",
|
||||||
13,
|
# 13,
|
||||||
days[4:],
|
# days[4:],
|
||||||
balance_offset=4,
|
# balance_offset=4,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
)
|
# )
|
||||||
|
|
||||||
rota_collections['no twighlights'].add_shifts(
|
# rota_collections['no twighlights'].add_shifts(
|
||||||
SingleShift(("exeter", ),
|
# SingleShift(("exeter", ),
|
||||||
"weekend_exeter",
|
# "weekend_exeter",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("truro", ),
|
# SingleShift(("truro", ),
|
||||||
"weekend_truro",
|
# "weekend_truro",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("torquay", ),
|
# SingleShift(("torquay", ),
|
||||||
"weekend_torquay",
|
# "weekend_torquay",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("plymouth", ),
|
# SingleShift(("plymouth", ),
|
||||||
"weekend_plymouth",
|
# "weekend_plymouth",
|
||||||
8,
|
# 8,
|
||||||
days[5:],
|
# days[5:],
|
||||||
workers_required=2,
|
# workers_required=2,
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
#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",
|
||||||
13,
|
# 13,
|
||||||
days[:4],
|
# days[:4],
|
||||||
balance_offset=5,
|
# balance_offset=5,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekend",
|
# "night_weekend",
|
||||||
13,
|
# 13,
|
||||||
days[4:],
|
# days[4:],
|
||||||
balance_offset=4,
|
# balance_offset=4,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
)
|
# )
|
||||||
|
|
||||||
rota_collections['no nights'].add_shifts(
|
# rota_collections['no nights'].add_shifts(
|
||||||
SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
||||||
SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
# SingleShift(("truro", ), "truro_twilight", 12.5, days[:5]),
|
||||||
SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
# SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
||||||
SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
# SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
||||||
SingleShift(("exeter", ),
|
# SingleShift(("exeter", ),
|
||||||
"weekend_exeter",
|
# "weekend_exeter",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("truro", ),
|
# SingleShift(("truro", ),
|
||||||
"weekend_truro",
|
# "weekend_truro",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("torquay", ),
|
# SingleShift(("torquay", ),
|
||||||
"weekend_torquay",
|
# "weekend_torquay",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("plymouth", ),
|
# SingleShift(("plymouth", ),
|
||||||
"weekend_plymouth",
|
# "weekend_plymouth",
|
||||||
8,
|
# 8,
|
||||||
days[5:],
|
# days[5:],
|
||||||
workers_required=2,
|
# workers_required=2,
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
#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 twighlights4 + 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",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("truro", ),
|
# SingleShift(("truro", ),
|
||||||
"weekend_truro",
|
# "weekend_truro",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("torquay", ),
|
# SingleShift(("torquay", ),
|
||||||
"weekend_torquay",
|
# "weekend_torquay",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("plymouth", ),
|
# SingleShift(("plymouth", ),
|
||||||
"weekend_plymouth",
|
# "weekend_plymouth",
|
||||||
8,
|
# 8,
|
||||||
days[5:],
|
# days[5:],
|
||||||
workers_required=2,
|
# workers_required=2,
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
#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",
|
||||||
12.25,
|
# 12.25,
|
||||||
days[:4],
|
# days[:4],
|
||||||
balance_offset=5,
|
# balance_offset=5,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekend",
|
# "night_weekend",
|
||||||
12.25,
|
# 12.25,
|
||||||
days[4:],
|
# days[4:],
|
||||||
balance_offset=4,
|
# balance_offset=4,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
)
|
# )
|
||||||
|
|
||||||
rota_collections['nights + proc twighlights3 + normal weekends'].add_shifts(
|
# rota_collections['nights + proc twighlights3 + normal weekends'].add_shifts(
|
||||||
SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
|
# SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
|
||||||
SingleShift(("exeter", ),
|
# SingleShift(("exeter", ),
|
||||||
"weekend_exeter",
|
# "weekend_exeter",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("truro", ),
|
# SingleShift(("truro", ),
|
||||||
"weekend_truro",
|
# "weekend_truro",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("torquay", ),
|
# SingleShift(("torquay", ),
|
||||||
"weekend_torquay",
|
# "weekend_torquay",
|
||||||
12.5,
|
# 12.5,
|
||||||
days[5:],
|
# days[5:],
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
SingleShift(("plymouth", ),
|
# SingleShift(("plymouth", ),
|
||||||
"weekend_plymouth",
|
# "weekend_plymouth",
|
||||||
8,
|
# 8,
|
||||||
days[5:],
|
# days[5:],
|
||||||
workers_required=2,
|
# workers_required=2,
|
||||||
assign_as_block=True),
|
# assign_as_block=True),
|
||||||
#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",
|
||||||
12.25,
|
# 12.25,
|
||||||
days[:4],
|
# days[:4],
|
||||||
balance_offset=5,
|
# balance_offset=5,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekend",
|
# "night_weekend",
|
||||||
12.25,
|
# 12.25,
|
||||||
days[4:],
|
# days[4:],
|
||||||
balance_offset=4,
|
# balance_offset=4,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
)
|
# )
|
||||||
|
|
||||||
rota_collections['nights + proc twighlights + proc weekends'].add_shifts(
|
# rota_collections['nights + proc twighlights + proc weekends'].add_shifts(
|
||||||
SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
|
# SingleShift((sites), "proc_twilight", 12.5, days[:5], workers_required=3),
|
||||||
SingleShift((sites), "proc_weekends", 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",
|
||||||
12.25,
|
# 12.25,
|
||||||
days[:4],
|
# days[:4],
|
||||||
balance_offset=5,
|
# balance_offset=5,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
SingleShift((sites),
|
# SingleShift((sites),
|
||||||
"night_weekend",
|
# "night_weekend",
|
||||||
12.25,
|
# 12.25,
|
||||||
days[4:],
|
# days[4:],
|
||||||
balance_offset=4,
|
# balance_offset=4,
|
||||||
balance_weighting=1,
|
# balance_weighting=1,
|
||||||
workers_required=3,
|
# workers_required=3,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
constraints=["night"]),
|
# constraints=["night"]),
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
load_leave = True
|
||||||
|
|
||||||
|
if load_leave:
|
||||||
|
import leave
|
||||||
|
|
||||||
|
leave = leave.load_leave()
|
||||||
|
|
||||||
# Import trainee data
|
# Import trainee data
|
||||||
import csv
|
import csv
|
||||||
@@ -347,16 +355,24 @@ with open('trainees.csv', newline='') as f:
|
|||||||
if fte == "0":
|
if fte == "0":
|
||||||
continue
|
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
|
end_date = end_date if end_date else None
|
||||||
oop = oop.split("-") if oop 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)
|
#print(nwds, end_date, oop)
|
||||||
for rot in rota_collections:
|
for rot in rota_collections:
|
||||||
|
|
||||||
Rota = rota_collections[rot]
|
Rota = rota_collections[rot]
|
||||||
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, not_available_to_work=leave_requests))
|
||||||
|
|
||||||
with open("rotas.html", "w") as f:
|
with open("rotas.html", "w") as f:
|
||||||
f.write("""<html>
|
f.write("""<html>
|
||||||
@@ -412,19 +428,19 @@ for rot in rota_collections:
|
|||||||
|
|
||||||
ResultsHolder.export_rota_to_csv("rotas/" + rot)
|
ResultsHolder.export_rota_to_csv("rotas/" + rot)
|
||||||
with open("rotas.html", "a") as f:
|
with open("rotas.html", "a") as f:
|
||||||
f.write("----------------------------")
|
f.write("<hr>")
|
||||||
f.write("<h2>{}</h2>".format(rot))
|
f.write("<h2>{}</h2>".format(rot))
|
||||||
f.write("----------------------------")
|
#f.write("----------------------------")
|
||||||
f.write("\n\n")
|
f.write("\n\n")
|
||||||
|
|
||||||
for s in Rota.shifts:
|
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")
|
||||||
f.write("\n\n")
|
f.write("\n\n")
|
||||||
|
|
||||||
f.write(ResultsHolder.get_worker_timetable_html(table_name=rot))
|
f.write(ResultsHolder.get_worker_timetable_html(table_name=rot))
|
||||||
f.write("\n\n")
|
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")
|
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)
|
#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()
|
#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 = $(".table-div");
|
||||||
|
|
||||||
tables.each((n, table) => {
|
|
||||||
console.log(table)
|
|
||||||
|
|
||||||
rows = $(table).find(".worker-row");
|
function generateExtra() {
|
||||||
console.log(rows)
|
$(".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");
|
worker_td = $(tr).find("td:first");
|
||||||
shifts = []
|
|
||||||
|
|
||||||
shift_tds.each((n, td) => {
|
worker = worker_td.attr("data-worker");
|
||||||
shifts.push($(td).attr("data-shift"));
|
|
||||||
})
|
|
||||||
|
|
||||||
shifts_unique = new Set(shifts);
|
shift_tds = $(tr).find(".rota-day");
|
||||||
shifts_unique.delete("");
|
shifts = []
|
||||||
//console.log(shifts_unique);
|
|
||||||
|
|
||||||
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++) {
|
weeks = shift_tds.length / 7;
|
||||||
//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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
shift_counts = {};
|
whole_weekends_worked = 0;
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
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
|
total_shifts = 0;
|
||||||
days_lost = days_lost + whole_weekends_worked
|
days_lost = 0;
|
||||||
console.log(worker, whole_weekends_worked);
|
shifts_unique.forEach(el => {
|
||||||
|
shift_counts[el] = shifts.count(el);
|
||||||
|
total_shifts = total_shifts + shifts.count(el);
|
||||||
|
|
||||||
//days_lost = days_lost + whole_weekends_worked;
|
// why like this?
|
||||||
//console.log(shift_counts);
|
if(el == "night_weekend") {
|
||||||
|
whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
|
||||||
|
}
|
||||||
|
|
||||||
workers[worker] = {
|
if($("#global-settings").length == 1) {
|
||||||
"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);
|
n = $(`#${el}-tdl`).val();
|
||||||
|
// console.log("SHIT", `${el}-tdl`, n)
|
||||||
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>`)
|
if(!isNaN(n)) {
|
||||||
|
days_lost = days_lost + shifts.count(el) * n;
|
||||||
})
|
}
|
||||||
|
|
||||||
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>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$(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
|
import itertools
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
from pyomo.environ import *
|
from pyomo.environ import *
|
||||||
from pyomo.opt import SolverFactory
|
from pyomo.opt import SolverFactory
|
||||||
|
|
||||||
from workers import Worker
|
from workers import Worker
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -89,6 +93,9 @@ class RotaBuilder(object):
|
|||||||
ltft_balance_offset=4,
|
ltft_balance_offset=4,
|
||||||
max_night_frequency=2,
|
max_night_frequency=2,
|
||||||
max_weekend_frequency=2):
|
max_weekend_frequency=2):
|
||||||
|
|
||||||
|
print("Start time: {}".format(datetime.datetime.now()))
|
||||||
|
|
||||||
self.shifts = [] # type List[SingleShift]
|
self.shifts = [] # type List[SingleShift]
|
||||||
|
|
||||||
self.days = days
|
self.days = days
|
||||||
@@ -98,9 +105,8 @@ 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:
|
if start_date.weekday() != 0:
|
||||||
raise ValueError(
|
raise ValueError("Start date {} must be a Mon (not a {})".format(
|
||||||
"Start date {} must be a Mon (not a {})".format(
|
start_date.isoformat(), days[start_date.weekday()]))
|
||||||
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
|
||||||
@@ -128,16 +134,31 @@ class RotaBuilder(object):
|
|||||||
self.constraint_options = {
|
self.constraint_options = {
|
||||||
"limit_to_1_st1_on_nights": True,
|
"limit_to_1_st1_on_nights": True,
|
||||||
"ensure_1_st4_plus_on_nights": True,
|
"ensure_1_st4_plus_on_nights": True,
|
||||||
|
"ensure_derriford_reg_for_nights": True,
|
||||||
"balance_nights": True,
|
"balance_nights": True,
|
||||||
"constrain_time_off_after_nights": True,
|
"constrain_time_off_after_nights": True,
|
||||||
"balance_nights_across_sites": True,
|
"balance_nights_across_sites": True,
|
||||||
|
"balance_bank_holidays": True,
|
||||||
"balance_blocks": True,
|
"balance_blocks": True,
|
||||||
"balance_shifts": True,
|
"balance_shifts": True,
|
||||||
"balance_weekends": True,
|
"balance_weekends": True,
|
||||||
"prevent_monday_after_full_weekends": False,
|
"max_weekends": False,
|
||||||
"prevent_fridays_before_full_weekends": True,
|
"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):
|
def build_model(self):
|
||||||
# Initialize model
|
# Initialize model
|
||||||
self.model = ConcreteModel()
|
self.model = ConcreteModel()
|
||||||
@@ -191,6 +212,26 @@ class RotaBuilder(object):
|
|||||||
initialize=0,
|
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
|
# Used to limit number of workers on night shift per site
|
||||||
# if we force a binary it will in effect hard constrain to < 2
|
# if we force a binary it will in effect hard constrain to < 2
|
||||||
# from a single site
|
# from a single site
|
||||||
@@ -362,7 +403,6 @@ class RotaBuilder(object):
|
|||||||
def work_request_init(model, wid, week, day, shift):
|
def work_request_init(model, wid, week, day, shift):
|
||||||
if (wid, week, day, shift) in self.work_requests:
|
if (wid, week, day, shift) in self.work_requests:
|
||||||
self.work_requests_map[(wid, week, day)] = shift
|
self.work_requests_map[(wid, week, day)] = shift
|
||||||
print((wid, week, day))
|
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -478,7 +518,6 @@ class RotaBuilder(object):
|
|||||||
def nightShiftMinST4Rule(model, week, shift):
|
def nightShiftMinST4Rule(model, week, shift):
|
||||||
single_workers = [w for w in self.workers if w.grade >= 4]
|
single_workers = [w for w in self.workers if w.grade >= 4]
|
||||||
if not single_workers:
|
if not single_workers:
|
||||||
print(single_workers)
|
|
||||||
return Constraint.Skip
|
return Constraint.Skip
|
||||||
return sum(model.shift_week_worker_assigned[shift, week, w.id]
|
return sum(model.shift_week_worker_assigned[shift, week, w.id]
|
||||||
for w in single_workers) >= 1
|
for w in single_workers) >= 1
|
||||||
@@ -493,6 +532,23 @@ class RotaBuilder(object):
|
|||||||
rule=nightShiftMinST4Rule,
|
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
|
# # 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
|
# # As 1 or 0 is optimum we can simply subtract 1 from the number
|
||||||
if self.constraint_options["balance_nights_across_sites"]:
|
if self.constraint_options["balance_nights_across_sites"]:
|
||||||
@@ -548,6 +604,14 @@ class RotaBuilder(object):
|
|||||||
# Most of our constraints apply per worker
|
# Most of our constraints apply per worker
|
||||||
for worker in self.workers:
|
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
|
# Count number of weekends an worker works
|
||||||
if self.constraint_options["balance_weekends"]:
|
if self.constraint_options["balance_weekends"]:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
@@ -555,6 +619,11 @@ class RotaBuilder(object):
|
|||||||
self.model.works_weekend[worker.id, week]
|
self.model.works_weekend[worker.id, week]
|
||||||
for week in self.weeks))
|
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)
|
# Balance shifts within required limits (set by balance_offset)
|
||||||
# If balance_offset is too restrictive (for the rota length)
|
# If balance_offset is too restrictive (for the rota length)
|
||||||
# no solution will be possible
|
# no solution will be possible
|
||||||
@@ -606,13 +675,6 @@ class RotaBuilder(object):
|
|||||||
self.model.works[worker.id, week, day, shift.name]
|
self.model.works[worker.id, week, day, shift.name]
|
||||||
for week, day in self.get_week_day_combinations()))
|
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
|
# Define shift_count_t1 and shift_count_t2 constraints for the object
|
||||||
# Thus bypassing the need for a quadratic solver
|
# Thus bypassing the need for a quadratic solver
|
||||||
# t1-t2 is the target
|
# t1-t2 is the target
|
||||||
@@ -628,7 +690,40 @@ class RotaBuilder(object):
|
|||||||
for shift in self.get_shifts()))
|
for shift in self.get_shifts()))
|
||||||
#if "night" not in shift.constraints))
|
#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"]:
|
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(
|
night_shift_target_number = sum(
|
||||||
worker.shift_target_number[shift.name]
|
worker.shift_target_number[shift.name]
|
||||||
for shift in self.get_shifts_with_constraint("night"))
|
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)
|
# self.model.night_shift_count_w[worker.id] >= 0)
|
||||||
|
|
||||||
# We use a similar method to balance the number of weekends worked
|
# 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"]:
|
if self.constraint_options["balance_weekends"]:
|
||||||
weekend_shift_target_number = sum(
|
weekend_shift_target_number = sum(
|
||||||
worker.shift_target_number[shift.name]
|
worker.shift_target_number[shift.name] /
|
||||||
for shift in self.get_shifts()
|
len(shift.shift_days) for shift in self.get_shifts()
|
||||||
if not set(("Sat", "Sun")).isdisjoint(shift.shift_days))
|
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
|
# min_shifts = weekend_shift_target_number - 20
|
||||||
# max_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] -
|
self.model.worker_weekend_count[worker.id] -
|
||||||
weekend_shift_target_number)
|
weekend_shift_target_number)
|
||||||
|
|
||||||
xU = 10
|
xU = self.constraint_options["max_weekends"]
|
||||||
|
|
||||||
xL = 1
|
xL = 1
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
inequality(
|
inequality(
|
||||||
@@ -750,7 +848,13 @@ class RotaBuilder(object):
|
|||||||
for week in week_blocks))
|
for week in week_blocks))
|
||||||
|
|
||||||
for week in self.weeks:
|
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"]:
|
"prevent_monday_after_full_weekends"]:
|
||||||
# Ignore last week
|
# Ignore last week
|
||||||
if week + 1 in self.weeks:
|
if week + 1 in self.weeks:
|
||||||
@@ -764,21 +868,53 @@ class RotaBuilder(object):
|
|||||||
sum(self.model.works[worker.id, week + 1, "Mon",
|
sum(self.model.works[worker.id, week + 1, "Mon",
|
||||||
shift.name]
|
shift.name]
|
||||||
for shift in self.get_shifts()))
|
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"]:
|
"prevent_fridays_before_full_weekends"]:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
2 >= sum(self.model.works[worker.id, week, "Sat",
|
2 >= sum(self.model.works[worker.id, week, "Sat",
|
||||||
shift.name]
|
shift.name]
|
||||||
for shift in self.get_shifts()
|
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",
|
sum(self.model.works[worker.id, week, "Sun",
|
||||||
shift.name]
|
shift.name]
|
||||||
for shift in self.get_shifts() if shift.name not in
|
for shift in self.get_shifts()
|
||||||
("night_weekend")) +
|
if "night" not in shift.constraints) +
|
||||||
sum(self.model.works[worker.id, week, "Fri",
|
sum(self.model.works[worker.id, week, "Fri",
|
||||||
shift.name]
|
shift.name]
|
||||||
for shift in self.get_shifts() if shift.name not in
|
for shift in self.get_shifts()
|
||||||
("night_weekend")))
|
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
|
# # 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
|
# # this is used (by the objective) to balance the total number of weekend shifts worked
|
||||||
@@ -959,6 +1095,17 @@ class RotaBuilder(object):
|
|||||||
else:
|
else:
|
||||||
night_shift_balancing = 0
|
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"]:
|
if self.constraint_options["balance_weekends"]:
|
||||||
weekend_balance_modifier_constant = 5
|
weekend_balance_modifier_constant = 5
|
||||||
weekend_shift_balancing = sum(
|
weekend_shift_balancing = sum(
|
||||||
@@ -977,7 +1124,7 @@ class RotaBuilder(object):
|
|||||||
for week, day, shift in self.get_all_shiftname_combinations())
|
for week, day, shift in self.get_all_shiftname_combinations())
|
||||||
|
|
||||||
# Work requsets
|
# 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(
|
work_requests = sum(
|
||||||
self.model.work_requests[worker.id, week, day, shift] *
|
self.model.work_requests[worker.id, week, day, shift] *
|
||||||
self.model.works[worker.id, week, day, shift] *
|
self.model.works[worker.id, week, day, shift] *
|
||||||
@@ -1006,9 +1153,10 @@ class RotaBuilder(object):
|
|||||||
blocks_balancing = 0
|
blocks_balancing = 0
|
||||||
|
|
||||||
#return weekend_shift_balancing + blocks_balancing
|
#return weekend_shift_balancing + blocks_balancing
|
||||||
|
#return bank_holiday_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 - 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)
|
# 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)
|
||||||
@@ -1273,6 +1421,10 @@ class RotaBuilder(object):
|
|||||||
t = "\n".join(l)
|
t = "\n".join(l)
|
||||||
return "Full time equivalent trainees by site:\n{}".format(t)
|
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):
|
class RotaResults(object):
|
||||||
def __init__(self, rota, results):
|
def __init__(self, rota, results):
|
||||||
@@ -1431,13 +1583,13 @@ class RotaResults(object):
|
|||||||
else:
|
else:
|
||||||
nwds = None
|
nwds = None
|
||||||
|
|
||||||
w = [
|
worker_targets = json.dumps(worker.shift_target_number)
|
||||||
"<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,
|
tds = []
|
||||||
worker.name, worker.fte, worker.fte_adj,
|
|
||||||
worker.end_date, worker.name, worker.grade, worker.fte)
|
|
||||||
]
|
|
||||||
n = 0
|
n = 0
|
||||||
|
|
||||||
|
shift_tds = []
|
||||||
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)
|
||||||
|
|
||||||
@@ -1445,19 +1597,21 @@ class RotaResults(object):
|
|||||||
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
|
||||||
|
title = "{} ({})".format(shift_name, d)
|
||||||
css_class = day
|
css_class = day
|
||||||
if model.available[worker.id, week, day] > 0:
|
if model.available[worker.id, week, day] > 0:
|
||||||
available = True
|
available = True
|
||||||
|
unavailable_reason = ""
|
||||||
else:
|
else:
|
||||||
available = False
|
available = False
|
||||||
css_class = "unavailable"
|
css_class = "unavailable"
|
||||||
|
unavailable_reason = self.rota.unavailable_to_work
|
||||||
|
|
||||||
bank_holiday = ""
|
bank_holiday = ""
|
||||||
if d in bank_holiday_map:
|
if d in bank_holiday_map:
|
||||||
@@ -1472,20 +1626,36 @@ class RotaResults(object):
|
|||||||
requests = " data-shift-request='{}'".format(
|
requests = " data-shift-request='{}'".format(
|
||||||
self.rota.work_requests_map[(worker.id, week, day)])
|
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>"
|
"<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,
|
.format(title, css_class, shift_name, available, d, week,
|
||||||
day, requests, bank_holiday, a))
|
day, requests, bank_holiday, a))
|
||||||
|
|
||||||
shift_count = ""
|
shift_count = ""
|
||||||
|
shift_count_dict = {}
|
||||||
for s in set(shifts):
|
for s in set(shifts):
|
||||||
shift_count = shift_count + "{}: {}, ".format(
|
c = shifts.count(s)
|
||||||
s, 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(
|
shift_count = shift_count + "#weekends_worked: {}\\#".format(
|
||||||
model.worker_weekend_count[worker.id].value)
|
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))
|
#timetable.append("<tr>{}</tr>".format("".join(w), shift_count))
|
||||||
|
|
||||||
# if show_prefs:
|
# if show_prefs:
|
||||||
|
|||||||
@@ -17,110 +17,90 @@ import operator
|
|||||||
|
|
||||||
use_neos = False
|
use_neos = False
|
||||||
|
|
||||||
weeks_to_rota = 8
|
weeks_to_rota = 26
|
||||||
|
|
||||||
time_to_run = 1200
|
time_to_run = 152000
|
||||||
allow = 500
|
allow = 2000
|
||||||
|
|
||||||
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
|
sites = ("truro", "exeter", "torquay", "barnstaple", "plymouth")
|
||||||
|
|
||||||
Rota = RotaBuilder(datetime.date(2020, 9, 7),
|
Rota = RotaBuilder(datetime.date(2020, 9, 7),
|
||||||
weeks_to_rota=weeks_to_rota,
|
weeks_to_rota=weeks_to_rota,
|
||||||
balance_offset_modifier=8,
|
balance_offset_modifier=1,
|
||||||
max_weekend_frequency=2,
|
max_weekend_frequency=2,
|
||||||
max_night_frequency=2)
|
max_night_frequency=2)
|
||||||
|
|
||||||
Rota.constraint_options["limit_to_1_st1_on_nights"] = False
|
Rota.constraint_options["limit_to_1_st1_on_nights"] = True
|
||||||
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = False
|
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = True
|
||||||
Rota.constraint_options["balance_nights"] = False
|
Rota.constraint_options["balance_nights"] = True
|
||||||
Rota.constraint_options["constrain_time_off_after_nights"] = False
|
Rota.constraint_options["constrain_time_off_after_nights"] = True
|
||||||
Rota.constraint_options["balance_nights_across_sites"] = False
|
Rota.constraint_options["balance_nights_across_sites"] = True
|
||||||
Rota.constraint_options["balance_blocks"] = True
|
Rota.constraint_options["balance_blocks"] = True
|
||||||
Rota.constraint_options["balance_weekends"] = True
|
Rota.constraint_options["balance_weekends"] = True
|
||||||
Rota.constraint_options["prevent_monday_after_full_weekends"] = False
|
Rota.constraint_options["avoid_st1_first_month"] = True
|
||||||
Rota.constraint_options["prevent_fridays_before_full_weekends"] = 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(
|
Rota.add_shifts(
|
||||||
# SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:5]),
|
SingleShift(("exeter", ), "exeter_twilight", 12.5, days[:4]),
|
||||||
# # SingleShift(("truro", ),
|
SingleShift(("truro", ), "truro_twilight", 12.5, days[:4]),
|
||||||
# # "truro_twilight",
|
SingleShift(("torquay", ),
|
||||||
# # 12.5,
|
"torquay_twilight",
|
||||||
# # days[:5],
|
12.5,
|
||||||
# # assign_as_block=False),
|
days[:4],
|
||||||
# #SingleShift(("torquay", ), "torquay_twilight", 12.5, days[:5]),
|
assign_as_block=True),
|
||||||
# #SingleShift(("plymouth", ), "plymouth_twilight", 12.5, days[:5]),
|
SingleShift(("plymouth", ),
|
||||||
# SingleShift(("exeter", ),
|
"plymouth_twilight",
|
||||||
# "weekend_exeter",
|
12.5,
|
||||||
# 12.5,
|
days[:5],
|
||||||
# days[5:],
|
workers_required=1,
|
||||||
# assign_as_block=False),
|
constraints=["max_2_consecutive_shifts_per_week"]),
|
||||||
# SingleShift(("truro", ),
|
SingleShift(("exeter", ),
|
||||||
# "weekend_truro",
|
"weekend_exeter",
|
||||||
# 12.5,
|
12.5,
|
||||||
# days[5:],
|
days[4:],
|
||||||
# force_as_block=True),
|
rota_on_nwds=True,
|
||||||
# # SingleShift(("torquay", ),
|
force_as_block=True),
|
||||||
# # "weekend_torquay",
|
SingleShift(("truro", ),
|
||||||
# # 12.5,
|
"weekend_truro",
|
||||||
# # days[5:],
|
12.5,
|
||||||
# # assign_as_block=True),
|
days[4:],
|
||||||
# # SingleShift(("plymouth", ),
|
balance_offset=1,
|
||||||
# # "weekend_plymouth",
|
rota_on_nwds=True,
|
||||||
# # 8,
|
force_as_block=True),
|
||||||
# # days[5:],
|
SingleShift(("torquay", ),
|
||||||
# # workers_required=2,
|
"weekend_torquay",
|
||||||
# # assign_as_block=True),
|
12.5,
|
||||||
# SingleShift((sites),
|
days[4:],
|
||||||
# "night_weekday",
|
rota_on_nwds=True,
|
||||||
# 12.25,
|
force_as_block=True),
|
||||||
# days[:4],
|
SingleShift(("plymouth", ),
|
||||||
# balance_offset=2,
|
"weekend_plymouth1",
|
||||||
# balance_weighting=1,
|
8,
|
||||||
# workers_required=3,
|
days[5:],
|
||||||
# force_as_block=False,
|
workers_required=1,
|
||||||
# rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
# constraints=["night"]),
|
force_as_block=True),
|
||||||
# SingleShift((sites),
|
SingleShift(("plymouth", ),
|
||||||
# "night_weekend",
|
"weekend_plymouth2",
|
||||||
# 12.25,
|
8,
|
||||||
# days[4:],
|
days[5:],
|
||||||
# balance_offset=2,
|
workers_required=1,
|
||||||
# balance_weighting=1,
|
rota_on_nwds=True,
|
||||||
# workers_required=3,
|
force_as_block=True),
|
||||||
# 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(
|
SingleShift(
|
||||||
(sites),
|
(sites),
|
||||||
"night_weekday",
|
"night_weekday",
|
||||||
12.25,
|
12.25,
|
||||||
days[:4],
|
days[:4],
|
||||||
balance_offset=2,
|
balance_offset=4,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
#hard_constrain_shift=False,
|
#hard_constrain_shift=False,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
@@ -132,7 +112,7 @@ Rota.add_shifts(
|
|||||||
"night_weekend",
|
"night_weekend",
|
||||||
12.25,
|
12.25,
|
||||||
days[4:],
|
days[4:],
|
||||||
balance_offset=2,
|
balance_offset=4,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
#hard_constrain_shift=False,
|
#hard_constrain_shift=False,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
@@ -143,6 +123,14 @@ Rota.add_shifts(
|
|||||||
|
|
||||||
use_test_workers = False
|
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
|
# Import trainee data
|
||||||
if use_test_workers:
|
if use_test_workers:
|
||||||
# Rota.add_workers([
|
# Rota.add_workers([
|
||||||
@@ -203,61 +191,82 @@ else:
|
|||||||
n = 0
|
n = 0
|
||||||
for row in reader:
|
for row in reader:
|
||||||
n = n + 1
|
n = n + 1
|
||||||
#print(row)
|
|
||||||
name, site, grade, fte, nwd, end_date, oop, extra = row
|
name, site, grade, fte, nwd, end_date, oop, extra = row
|
||||||
|
|
||||||
# Ignore trainees if fte == 0 (what are they doing here anyway)
|
# Ignore trainees if fte == 0 (what are they doing here anyway)
|
||||||
if int(fte) < 1:
|
if int(fte) < 1:
|
||||||
continue
|
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
|
end_date = end_date if end_date else None
|
||||||
oop = oop.split("-") if oop 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)
|
#print(nwds, end_date, oop)
|
||||||
# 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")):
|
# if name.startswith(("Alex", "Michal", "Ross")):
|
||||||
Rota.add_worker(
|
# Rota.add_worker(
|
||||||
Worker(Rota,
|
# Worker(Rota,
|
||||||
n,
|
# n,
|
||||||
name,
|
# name,
|
||||||
site.lower(),
|
# site.lower(),
|
||||||
int(grade[2]),
|
# int(grade[2]),
|
||||||
int(fte),
|
# int(fte),
|
||||||
nwds,
|
# nwds,
|
||||||
end_date,
|
# end_date,
|
||||||
oop,
|
# oop,
|
||||||
work_requests=(("2020/09/07", "night_weekday"), )))
|
# work_requests=(("2020/09/07", "night_weekday"), )))
|
||||||
continue
|
# continue
|
||||||
if "Salma" in name:
|
# if "Salma" in name:
|
||||||
Rota.add_worker(
|
# Rota.add_worker(
|
||||||
Worker(Rota,
|
# Worker(Rota,
|
||||||
n,
|
# n,
|
||||||
name,
|
# name,
|
||||||
site.lower(),
|
# site.lower(),
|
||||||
int(grade[2]),
|
# int(grade[2]),
|
||||||
int(fte),
|
# int(fte),
|
||||||
nwds,
|
# nwds,
|
||||||
end_date,
|
# end_date,
|
||||||
oop,
|
# oop,
|
||||||
work_requests=(("2020/09/11", "night_weekend"), )))
|
# work_requests=(("2020/12/25",
|
||||||
continue
|
# "exeter_twilight"), )))
|
||||||
|
# continue
|
||||||
Rota.add_worker(
|
Rota.add_worker(
|
||||||
Worker(Rota, n, name, site.lower(), int(grade[2]), int(fte),
|
Worker(Rota,
|
||||||
nwds, end_date, oop))
|
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_shifts_and_workers()
|
||||||
|
|
||||||
Rota.build_model()
|
Rota.build_model()
|
||||||
|
|
||||||
#print(Rota.get_worker_details())
|
#print(Rota.get_worker_details())
|
||||||
print(1)
|
print("Setting up solver")
|
||||||
opt = SolverFactory('cbc') # choose a solver
|
opt = SolverFactory('cbc') # choose a solver
|
||||||
#opt = SolverFactory('ipopt') # choose a solver
|
#opt = SolverFactory('ipopt') # choose a solver
|
||||||
|
|
||||||
print(2)
|
print("Solving")
|
||||||
if use_neos:
|
if use_neos:
|
||||||
solver_manager = SolverManagerFactory('neos') # Solve in neos server
|
solver_manager = SolverManagerFactory('neos') # Solve in neos server
|
||||||
results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log")
|
results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log")
|
||||||
@@ -268,6 +277,7 @@ else:
|
|||||||
options={
|
options={
|
||||||
"seconds": time_to_run,
|
"seconds": time_to_run,
|
||||||
"allow": allow,
|
"allow": allow,
|
||||||
|
"threads": 4,
|
||||||
},
|
},
|
||||||
logfile="test.log"
|
logfile="test.log"
|
||||||
) # solve the model with the, options="seconds=60" selected solver
|
) # 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(
|
worker_timetable_brief = ResultsHolder.get_worker_timetable_brief(
|
||||||
show_prefs=False, show_unavailable=False)
|
show_prefs=False, show_unavailable=False)
|
||||||
|
|
||||||
print(worker_timetable_brief)
|
#print(worker_timetable_brief)
|
||||||
print(Rota.get_workers_total_fte())
|
#print(Rota.get_workers_total_fte())
|
||||||
|
|
||||||
ResultsHolder.get_weekend_details()
|
ResultsHolder.get_weekend_details()
|
||||||
#ResultsHolder.get_night_details()
|
#ResultsHolder.get_night_details()
|
||||||
@@ -293,6 +303,8 @@ ResultsHolder.get_weekend_details()
|
|||||||
with open("test.html", "w") as f:
|
with open("test.html", "w") as f:
|
||||||
f.write(ResultsHolder.get_worker_timetable_html(True))
|
f.write(ResultsHolder.get_worker_timetable_html(True))
|
||||||
|
|
||||||
|
ResultsHolder.export_rota_to_csv()
|
||||||
|
|
||||||
# for i in ResultsHolder.rota.model.blocks_worker_shift_count:
|
# for i in ResultsHolder.rota.model.blocks_worker_shift_count:
|
||||||
# n = ResultsHolder.rota.model.blocks_worker_shift_count[i].value
|
# n = ResultsHolder.rota.model.blocks_worker_shift_count[i].value
|
||||||
|
|
||||||
|
|||||||
+78
-17
@@ -7,20 +7,27 @@ table {
|
|||||||
/* margin-top: 50px; */
|
/* margin-top: 50px; */
|
||||||
padding-top: 100px;
|
padding-top: 100px;
|
||||||
text-align:center;
|
text-align:center;
|
||||||
table-layout : fixed;
|
/* table-layout : fixed; */
|
||||||
/* width:150px */
|
/* width:150px */
|
||||||
width: 200%;
|
/* width: 200%; */
|
||||||
overflow-x: auto;
|
/* overflow-x: auto;
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative; */
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#table-div {
|
.table-div {
|
||||||
overflow-x: auto;
|
/* overflow-x: auto; */
|
||||||
|
width: 80%;
|
||||||
|
overflow-x: scroll;
|
||||||
|
margin-left: 200px;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
.table-div th {
|
||||||
text-align:center;
|
text-align:center;
|
||||||
white-space:nowrap;
|
white-space:nowrap;
|
||||||
transform: rotate(90deg) translateX(-100px);
|
transform: rotate(90deg) translateX(-100px);
|
||||||
@@ -28,11 +35,11 @@ th {
|
|||||||
top:0;
|
top:0;
|
||||||
|
|
||||||
}
|
}
|
||||||
th p {
|
.table-div th p {
|
||||||
margin:0 -100% ;
|
margin:0 -100% ;
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
}
|
}
|
||||||
th p:before{
|
.table-div th p:before{
|
||||||
content:'';
|
content:'';
|
||||||
width:0;
|
width:0;
|
||||||
padding-top:110%;/* takes width as reference, + 10% for faking some extra padding */
|
padding-top:110%;/* takes width as reference, + 10% for faking some extra padding */
|
||||||
@@ -40,20 +47,20 @@ th p:before{
|
|||||||
vertical-align:middle;
|
vertical-align:middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
td, th {
|
.table-div td, th {
|
||||||
max-width: 10px;
|
max-width: 10px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
.table-div tr {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
.table-div tr:hover {
|
||||||
background-color: lightblue;
|
background-color: lightblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:hover .unavailable{
|
.table-div tr:hover .unavailable{
|
||||||
|
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
}
|
}
|
||||||
@@ -70,15 +77,14 @@ tr:hover .unavailable{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bank-holiday {
|
.bank-holiday {
|
||||||
border: solid 1px blue;
|
border-bottom: dotted 1px blue;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
th.worker {
|
/* th.worker {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
/* width: 100px; */
|
} */
|
||||||
}
|
|
||||||
|
|
||||||
.plymouth {
|
.plymouth {
|
||||||
color: #9169FF;
|
color: #9169FF;
|
||||||
@@ -102,6 +108,8 @@ th.worker {
|
|||||||
.worker-summary {
|
.worker-summary {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
|
overflow-x: visible;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.site-summary {
|
.site-summary {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -125,3 +133,56 @@ th.worker {
|
|||||||
.training-days-lost {
|
.training-days-lost {
|
||||||
padding-top: 5px;
|
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:
|
class Worker:
|
||||||
def __init__(self,
|
def __init__(
|
||||||
Rota,
|
self,
|
||||||
id,
|
Rota,
|
||||||
name,
|
id,
|
||||||
site,
|
name,
|
||||||
grade,
|
site,
|
||||||
fte=100,
|
grade,
|
||||||
nwd=None,
|
fte=100,
|
||||||
end_date=None,
|
nwd=None,
|
||||||
oop=None,
|
end_date=None,
|
||||||
pref_not_to_work=None,
|
oop=None,
|
||||||
work_requests=None,):
|
not_available_to_work=None,
|
||||||
|
pref_not_to_work=None,
|
||||||
|
work_requests=None,
|
||||||
|
night_at_derriford=0,
|
||||||
|
):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.site = site
|
self.site = site
|
||||||
@@ -26,6 +30,7 @@ class Worker:
|
|||||||
self.fte = fte
|
self.fte = fte
|
||||||
self.nwd = nwd
|
self.nwd = nwd
|
||||||
self.proportion_rota_to_work = 1
|
self.proportion_rota_to_work = 1
|
||||||
|
self.night_at_derriford = night_at_derriford
|
||||||
|
|
||||||
self.shift_target_number = defaultdict(int)
|
self.shift_target_number = defaultdict(int)
|
||||||
|
|
||||||
@@ -54,7 +59,6 @@ class Worker:
|
|||||||
end_oop_date = datetime.datetime.strptime(end_oop,
|
end_oop_date = datetime.datetime.strptime(end_oop,
|
||||||
"%Y/%m/%d").date()
|
"%Y/%m/%d").date()
|
||||||
|
|
||||||
print(start_oop_date, end_oop_date)
|
|
||||||
if end_oop_date > Rota.rota_end_date:
|
if end_oop_date > Rota.rota_end_date:
|
||||||
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:
|
if pref_not_to_work is not None:
|
||||||
# loop throught dates converting to week / day combination
|
# loop throught dates converting to week / day combination
|
||||||
for date in pref_not_to_work:
|
for 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
|
week = days_from_start // 7 + 1
|
||||||
day = Rota.days[(days_from_start % 7)]
|
day = Rota.days[(days_from_start % 7)]
|
||||||
# Weight the value to take into account the number of preferences
|
# 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
|
# 1 is added to the total number of requests to ensure they do not outweight
|
||||||
# a single (or fewer) request(s)
|
# a single (or fewer) request(s)
|
||||||
Rota.pref_not_to_work[(self.id, week, day)] = 1 / (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:
|
if work_requests is not None:
|
||||||
for date, shift in work_requests:
|
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
|
week = days_from_start // 7 + 1
|
||||||
day = Rota.days[(days_from_start % 7)]
|
day = Rota.days[(days_from_start % 7)]
|
||||||
Rota.work_requests.add((self.id, week, day, shift))
|
Rota.work_requests.add((self.id, week, day, 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
|
||||||
|
|
||||||
# We had to adjust the full time equivalent for people who CCT / leave the rota early
|
# 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
|
self.fte_adj = self.fte * self.proportion_rota_to_work
|
||||||
|
|
||||||
|
# print(self.name, self.nwd)
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return (self.site, self.grade, self.fte_adj, self.name) < (
|
return (self.site, self.grade, self.fte_adj, self.name) < (
|
||||||
other.site,
|
other.site,
|
||||||
|
|||||||
Reference in New Issue
Block a user