This commit is contained in:
Ross
2020-12-18 17:51:59 +00:00
parent 74484db6c6
commit eddb6a0f04
7 changed files with 1026 additions and 566 deletions
+56
View File
@@ -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()
+267 -251
View File
@@ -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()
+244 -119
View File
@@ -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();
}));
})
+210 -40
View File
@@ -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:
+134 -122
View File
@@ -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
View File
@@ -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
View File
@@ -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,