From eddb6a0f04763efb8a8866e0835e8d0696ae379b Mon Sep 17 00:00:00 2001 From: Ross Date: Fri, 18 Dec 2020 17:51:59 +0000 Subject: [PATCH] go --- leave.py | 56 ++++++ run.py | 518 ++++++++++++++++++++++++++------------------------ s.js | 363 +++++++++++++++++++++++------------ shifts.py | 250 ++++++++++++++++++++---- test.py | 256 +++++++++++++------------ timetable.css | 95 +++++++-- workers.py | 54 ++++-- 7 files changed, 1026 insertions(+), 566 deletions(-) create mode 100644 leave.py diff --git a/leave.py b/leave.py new file mode 100644 index 0000000..ec1ec89 --- /dev/null +++ b/leave.py @@ -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() \ No newline at end of file diff --git a/run.py b/run.py index 8235dfa..d0ef758 100644 --- a/run.py +++ b/run.py @@ -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(""" @@ -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("
") f.write("

{}

".format(rot)) - f.write("----------------------------") + #f.write("----------------------------") f.write("\n\n") for s in Rota.shifts: - f.write("
{}
".format(s.get_shift_summary())) + f.write("
{}
".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("
{}
".format(worker_shift_summary)) + f.write("
{}
".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() diff --git a/s.js b/s.js index 6663ec5..7cd22cf 100644 --- a/s.js +++ b/s.js @@ -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(`
Total shifts: ${total_shifts}, Weekends: ${weekends_worked}, Training days lost: ${days_lost}, ${sc}
`) - - }) - - 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("
Average shifts for a 100% FTE trainee
"); - - for (const site in sites) { - console.log(site, $("div")); - $(table).find(".site-summary").append(`
${site}
`); - - for (const shift in sites[site]) { - console.log(sites[site][shift]); - average = mean(sites[site][shift]) - $(table).find(`.site-${site}`).append(`
${shift}: ${average.toFixed(2)}
`); - } - - days_lost_avg = mean(sites_days_lost[site]); - $(table).find(`.site-${site}`).append(`
Training days lost: ${days_lost_avg.toFixed(2)}
`); - } + n = $(`#${el}-tdl`).val(); + // console.log("SHIT", `${el}-tdl`, n) + if(!isNaN(n)) { + days_lost = days_lost + shifts.count(el) * n; + } - $(table).before($("").click(() => { - $(table).find(".rota-day").toggle(); - $(table).find(".worker-summary").toggle(); + } - })) -}); \ No newline at end of file + // // 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(`
Total shifts: ${total_shifts}, Weekends: ${weekends_worked},
`) + + }) + + 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("
Average shifts for a 100% FTE trainee
"); + + // for(const site in sites) { + // $(table).find(".site-summary").append(`
${site}
`); + + // for(const shift in sites[site]) { + // average = mean(sites[site][shift]) + // $(table).find(`.site-${site}`).append(`
${shift}: ${average.toFixed(2)}
`); + // } + + // days_lost_avg = mean(sites_days_lost[site]); + // $(table).find(`.site-${site}`).append(`
Training days lost: ${days_lost_avg.toFixed(2)}
`); + // } + + 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("
") + +$("table.tsummary").append(""); +h = $("table.tsummary tr.header"); +h.append(`Worker`); +h.append(`Site`); +h.append(`FTE`); +h.append(`FTE (adj)`); + + +oshifts.forEach((s) => { + h.append(`${s}`); + +}); + + + +$(".table-div .worker-row .worker").each((n, tr) => { + //worker_td = $(tr).find("td").first(); + console.log(tr); + jtr = $(tr); + row = $(""); + row.append(`${jtr.data("worker")}`) + row.append(`${jtr.data("site")}`) + row.append(`${jtr.data("fte")}`) + row.append(`${parseFloat(jtr.data("fte_adj")).toFixed(2)}`) + + 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(`${c} (${shift_targets[s].toFixed(2)})`) + + } else { + row.append(`-`) + } + }) + console.log(shift_counts); + + $("table.tsummary").append(row) +}) + +//$("table.summary") + +// $("body").prepend("
Settings:
Customise training days lost per shift (you need to press recalculate for changes to take effect)
") + +// 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(``); +// } + + +// }) +// $("#global-settings").append($("").click(() => { +// generateExtra(); +// })) + +// generateExtra(); + +$(".table-div + pre").each((n, pre) => { + + $(pre).before($("").click(() => { + $(pre).toggle(); + })); + +}) \ No newline at end of file diff --git a/shifts.py b/shifts.py index 2fd1c2b..82ad588 100644 --- a/shifts.py +++ b/shifts.py @@ -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 = [ - "{} ({}) [{}]" - .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( "{}" .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 = "{} ({}) [{}]".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("{}".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("{}{}".format( + worker_td, "".join(shift_tds))) #timetable.append("{}".format("".join(w), shift_count)) # if show_prefs: diff --git a/test.py b/test.py index d367987..de30921 100644 --- a/test.py +++ b/test.py @@ -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 diff --git a/timetable.css b/timetable.css index 3aec61e..9c852fe 100644 --- a/timetable.css +++ b/timetable.css @@ -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 */ +} \ No newline at end of file diff --git a/workers.py b/workers.py index 854f746..63dbaa8 100644 --- a/workers.py +++ b/workers.py @@ -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,