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