many improvements
This commit is contained in:
@@ -0,0 +1,311 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
|
||||||
|
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
"truro",
|
||||||
|
"exeter",
|
||||||
|
"torbay",
|
||||||
|
"barnstaple",
|
||||||
|
"plymouth",
|
||||||
|
"taunton",
|
||||||
|
"proc",
|
||||||
|
"truro no proc",
|
||||||
|
)
|
||||||
|
|
||||||
|
from rota.workers import (
|
||||||
|
Worker,
|
||||||
|
NotAvailableToWork,
|
||||||
|
NonWorkingDays,
|
||||||
|
WorkRequests,
|
||||||
|
PreferenceNotToWork,
|
||||||
|
OutOfProgramme,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
suspend_on_finish = False
|
||||||
|
solve = True
|
||||||
|
time_to_run = 258500
|
||||||
|
# allow = 5000
|
||||||
|
ratio = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
start_date = datetime.date(2022, 9, 5)
|
||||||
|
|
||||||
|
Rota = RotaBuilder(start_date, weeks_to_rota=12)
|
||||||
|
Rota.constraint_options["balance_weekends"] = True
|
||||||
|
Rota.constraint_options["max_night_frequency"] = 1
|
||||||
|
Rota.constraint_options["max_weekend_frequency"] = 1
|
||||||
|
# Rota.constraint_options["avoid_st2_first_month"] = True
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("exeter", "exeter twilights and weekends"),
|
||||||
|
name="exeter_twilight",
|
||||||
|
length=12.5,
|
||||||
|
days=days[:5],
|
||||||
|
balance_offset=3,
|
||||||
|
constraint=[{"name": "max_shifts_per_week", "options": 2}],
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("truro", "truro twilights"),
|
||||||
|
name="truro_twilight",
|
||||||
|
length=12.5,
|
||||||
|
days=days[:4],
|
||||||
|
balance_offset=3,
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("torbay", "torbay twilights"),
|
||||||
|
name="torbay_twilight",
|
||||||
|
length=12.5,
|
||||||
|
days=days[:4],
|
||||||
|
balance_offset=5,
|
||||||
|
assign_as_block=True,
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("plymouth", "plymouth twilights"),
|
||||||
|
name="plymouth_twilight",
|
||||||
|
length=12.5,
|
||||||
|
days=days[:5],
|
||||||
|
balance_offset=3,
|
||||||
|
workers_required=1,
|
||||||
|
constraint=[{"name": "max_shifts_per_week", "options": 2}],
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("exeter", "exeter twilights and weekends"),
|
||||||
|
name="weekend_exeter",
|
||||||
|
length=12.5,
|
||||||
|
days=days[5:],
|
||||||
|
balance_offset=4,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
force_as_block=True,
|
||||||
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}, {"name": "single_shift_block_per_week"}],
|
||||||
|
#constraint=[{"name": "preclear2"}, {"name": "postclear2"},],
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("truro",),
|
||||||
|
name="weekend_truro",
|
||||||
|
length=12.5,
|
||||||
|
days=days[4:],
|
||||||
|
balance_offset=4,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
force_as_block=True,
|
||||||
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
|
# force_as_block_unless_nwd=True
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("torbay",),
|
||||||
|
name="weekend_torbay",
|
||||||
|
length=12.5,
|
||||||
|
days=days[4:],
|
||||||
|
balance_offset=4,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
force_as_block=True,
|
||||||
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
|
# force_as_block_unless_nwd=True
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("plymouth",),
|
||||||
|
name="weekend_plymouth1",
|
||||||
|
length=8,
|
||||||
|
days=days[5:],
|
||||||
|
balance_offset=4,
|
||||||
|
workers_required=1,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
force_as_block=True,
|
||||||
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("plymouth",),
|
||||||
|
name="weekend_plymouth2",
|
||||||
|
length=8,
|
||||||
|
days=days[5:],
|
||||||
|
balance_offset=4,
|
||||||
|
workers_required=1,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
force_as_block=True,
|
||||||
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("plymouth", "plymouth twilights"),
|
||||||
|
name="plymouth_bank_holidays",
|
||||||
|
length=8,
|
||||||
|
days=days[5:],
|
||||||
|
balance_offset=2,
|
||||||
|
workers_required=1,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
force_as_block=False,
|
||||||
|
bank_holidays_only=True,
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=(sites[:-1]),
|
||||||
|
name="night_weekday",
|
||||||
|
length=12.25,
|
||||||
|
days=days[:4],
|
||||||
|
balance_offset=4.9,
|
||||||
|
balance_weighting=1,
|
||||||
|
# hard_constrain_shift=False,
|
||||||
|
workers_required=3,
|
||||||
|
force_as_block=True,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
constraint=[
|
||||||
|
{"name": "night"},
|
||||||
|
{"name": "preclear2"},
|
||||||
|
{"name": "postclear2"},
|
||||||
|
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=(sites[:-1]),
|
||||||
|
name="night_weekend",
|
||||||
|
length=12.25,
|
||||||
|
days=days[4:],
|
||||||
|
balance_offset=3.9,
|
||||||
|
balance_weighting=1,
|
||||||
|
# hard_constrain_shift=False,
|
||||||
|
workers_required=3,
|
||||||
|
force_as_block=True,
|
||||||
|
rota_on_nwds=True,
|
||||||
|
constraint=[
|
||||||
|
{"name": "night"},
|
||||||
|
{"name": "preclear2"},
|
||||||
|
{"name": "postclear2"},
|
||||||
|
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([2], [1, 2, 3, 4], ["night_weekday", "night_weekend"])
|
||||||
|
|
||||||
|
load_leave = True
|
||||||
|
Rota.build_shifts()
|
||||||
|
Rota.add_grade_constraint_by_week([2], [1, 2], Rota.get_shift_names())
|
||||||
|
|
||||||
|
if load_leave:
|
||||||
|
import leave
|
||||||
|
|
||||||
|
workers = leave.load_leave(Rota)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
for worker in workers:
|
||||||
|
n = n + 1
|
||||||
|
worker_name = worker
|
||||||
|
w = workers[worker]
|
||||||
|
site = w["site"]
|
||||||
|
grade = w["grade"]
|
||||||
|
fte = float(w["fte"]) * 100
|
||||||
|
nwd = w["nwd"]
|
||||||
|
end_date = w["end_date"]
|
||||||
|
start_date = w["start_date"]
|
||||||
|
oop = w["oop"]
|
||||||
|
grade = w["grade"]
|
||||||
|
pair = w["pair"]
|
||||||
|
# name, site, grade, fte, nwd, end_date, oop, twi, twi_we, night, night_we, pw1, pw2, extra = row
|
||||||
|
|
||||||
|
# Ignore trainees if fte == 0 (what are they doing here anyway)
|
||||||
|
if fte == 0 or fte == "0" or fte == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
nwds = []
|
||||||
|
if nwd:
|
||||||
|
for i in nwd.split(","):
|
||||||
|
nwd_start_date = Rota.start_date
|
||||||
|
nwd_end_date = Rota.rota_end_date
|
||||||
|
|
||||||
|
if "[" in i:
|
||||||
|
a, b = i.split("[")[1][:-1].split("-")
|
||||||
|
nwd_start_date = datetime.datetime.strptime(a, "%d/%m/%y").date()
|
||||||
|
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%y").date()
|
||||||
|
|
||||||
|
nwds.append(
|
||||||
|
NonWorkingDays(
|
||||||
|
day=i.split("[")[0].strip()[:3].capitalize(),
|
||||||
|
start_date=nwd_start_date,
|
||||||
|
end_date=nwd_end_date,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# nwds = [i.strip()[:3].capitalize() for i in nwd.split("/")] if nwd else None
|
||||||
|
end_date = end_date if end_date else None
|
||||||
|
oop = oop.split("-") if oop else None
|
||||||
|
|
||||||
|
previous_shifts = {}
|
||||||
|
# if twi:
|
||||||
|
# worked = twi.split(" ")[0]
|
||||||
|
# allocated = twi.split(" ")[1][1:-1]
|
||||||
|
# previous_shifts["{}_twilight".format(site.lower())] = (worked, allocated)
|
||||||
|
# if twi_we:
|
||||||
|
# worked = twi_we.split(" ")[0]
|
||||||
|
# allocated = twi_we.split(" ")[1][1:-1]
|
||||||
|
# previous_shifts["weekend_{}".format(site.lower())] = (worked, allocated)
|
||||||
|
# if night:
|
||||||
|
# worked = night.split(" ")[0]
|
||||||
|
# allocated = night.split(" ")[1][1:-1]
|
||||||
|
# previous_shifts["night_weekday".format(site.lower())] = (worked, allocated)
|
||||||
|
# if night_we:
|
||||||
|
# worked = night_we.split(" ")[0]
|
||||||
|
# allocated = night_we.split(" ")[1][1:-1]
|
||||||
|
# previous_shifts["night_weekend".format(site.lower())] = (worked, allocated)
|
||||||
|
# if pw1:
|
||||||
|
# worked = pw1.split(" ")[0]
|
||||||
|
# allocated = pw1.split(" ")[1][1:-1]
|
||||||
|
# previous_shifts["weekend_plymouth1"] = (worked, allocated)
|
||||||
|
# if pw2:
|
||||||
|
# worked = pw2.split(" ")[0]
|
||||||
|
# allocated = pw2.split(" ")[1][1:-1]
|
||||||
|
# previous_shifts["weekend_plymouth2"] = (worked, allocated)
|
||||||
|
|
||||||
|
leave_requests = w["leave"]
|
||||||
|
work_requests = w["requests"]
|
||||||
|
site_pref = w["site_pref"]
|
||||||
|
|
||||||
|
Rota.add_worker(
|
||||||
|
Worker(
|
||||||
|
name=worker_name,
|
||||||
|
site=site.lower(),
|
||||||
|
grade=int(grade[2]),
|
||||||
|
id=n,
|
||||||
|
fte=int(fte),
|
||||||
|
nwds=nwds,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
opp=oop,
|
||||||
|
not_available_to_work=leave_requests,
|
||||||
|
work_requests=work_requests,
|
||||||
|
remote_site=site_pref,
|
||||||
|
previous_shifts=previous_shifts,
|
||||||
|
pair=pair,
|
||||||
|
shift_balance_extra=w["shift_balance_extra"],
|
||||||
|
bank_holiday_extra=w["bank_holiday_extra"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_workers()
|
||||||
|
Rota.build_model()
|
||||||
|
|
||||||
|
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
|
||||||
|
|
||||||
|
Rota.solve_model(options=solver_options)
|
||||||
|
|
||||||
|
# print(Rota.get_worker_details())
|
||||||
|
|
||||||
|
# optimizer = SolverFactory('cbc')
|
||||||
|
# result = optimizer.solve(prob,tee=True)
|
||||||
|
# result.Solver.Status = SolverStatus.warning
|
||||||
|
# prob.solutions.load_from(result)
|
||||||
|
|
||||||
|
# ResultsHolder = RotaResults(Rota)
|
||||||
|
|
||||||
|
# worker_timetable_brief = ResultsHolder.get_worker_timetable_brief(
|
||||||
|
# show_prefs=False, show_unavailable=False
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# print(worker_timetable_brief)
|
||||||
|
|
||||||
|
|
||||||
|
Rota.export_rota_to_html("proc_rota")
|
||||||
|
# Rota.export_rota_to_csv("rota")
|
||||||
|
|
||||||
|
if suspend_on_finish:
|
||||||
|
os.system("systemctl suspend")
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
import csv
|
import csv
|
||||||
|
from datetime import datetime
|
||||||
import re
|
import re
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
|
||||||
|
from rota.workers import Worker, NotAvailableToWork, NonWorkingDays, WorkRequests, PreferenceNotToWork, OutOfProgramme
|
||||||
|
|
||||||
date_re = r"[\d]{1,2}\/[\d]{1,2}\/[\d]{2}"
|
date_re = r"[\d]{1,2}\/[\d]{1,2}\/[\d]{2}"
|
||||||
|
|
||||||
|
|
||||||
def load_leave(Rota, use_live_rota):
|
def load_leave(Rota):
|
||||||
|
|
||||||
file_name = "leave.csv"
|
with Session() as s:
|
||||||
|
|
||||||
with open(file_name, newline="") as f:
|
|
||||||
shifts = Rota.get_shift_names()
|
shifts = Rota.get_shift_names()
|
||||||
|
|
||||||
workers = {}
|
workers = {}
|
||||||
|
|
||||||
reader = csv.reader(f, delimiter=",", quotechar='"')
|
download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vS49eVS0f86wUJIhXMq7tMPZfAEJZatER09JLu0xnlTjDBkC-54DkAPT3I0JVXDQXNG6AIh_fVeIRS3/pub?gid=0&single=true&output=csv")
|
||||||
|
decoded_content = download.content.decode('utf-8')
|
||||||
|
|
||||||
|
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
|
||||||
|
|
||||||
leave = {}
|
leave = {}
|
||||||
requests = {}
|
requests = {}
|
||||||
@@ -21,7 +27,7 @@ def load_leave(Rota, use_live_rota):
|
|||||||
|
|
||||||
order = []
|
order = []
|
||||||
|
|
||||||
n = 0
|
n = 1
|
||||||
for row in reader:
|
for row in reader:
|
||||||
row_title = row[0]
|
row_title = row[0]
|
||||||
r = row[2:]
|
r = row[2:]
|
||||||
@@ -71,8 +77,20 @@ def load_leave(Rota, use_live_rota):
|
|||||||
elif row_title == "Flexible NWD":
|
elif row_title == "Flexible NWD":
|
||||||
worker["flexible_nwd"] = lower_item
|
worker["flexible_nwd"] = lower_item
|
||||||
|
|
||||||
elif row_title == "End Date":
|
elif row_title in ("End Date", "CCT date"):
|
||||||
worker["end_date"] = lower_item
|
if lower_item:
|
||||||
|
date = datetime.strptime(lower_item, "%d/%m/%y").date()
|
||||||
|
worker["end_date"] = date
|
||||||
|
else:
|
||||||
|
worker["end_date"] = None
|
||||||
|
|
||||||
|
|
||||||
|
elif row_title in ("Start date",):
|
||||||
|
if lower_item:
|
||||||
|
date = datetime.strptime(lower_item, "%d/%m/%y").date()
|
||||||
|
worker["start_date"] = date
|
||||||
|
else:
|
||||||
|
worker["start_date"] = None
|
||||||
|
|
||||||
elif row_title == "OOP":
|
elif row_title == "OOP":
|
||||||
worker["oop"] = lower_item
|
worker["oop"] = lower_item
|
||||||
@@ -97,18 +115,22 @@ def load_leave(Rota, use_live_rota):
|
|||||||
|
|
||||||
elif row_title == "bank_holidays_worked":
|
elif row_title == "bank_holidays_worked":
|
||||||
if lower_item:
|
if lower_item:
|
||||||
print("BH", lower_item)
|
|
||||||
worker["bank_holiday_extra"] = int(lower_item)
|
worker["bank_holiday_extra"] = int(lower_item)
|
||||||
|
|
||||||
elif re.match(date_re, row_title) is not None:
|
elif re.match(date_re, row_title) is not None:
|
||||||
|
|
||||||
if lower_item != "":
|
if lower_item != "":
|
||||||
|
date = datetime.strptime(row[0], "%d/%m/%y").date()
|
||||||
# This may be easier to do as a dict
|
# This may be easier to do as a dict
|
||||||
if lower_item in shifts:
|
if lower_item in shifts:
|
||||||
worker["requests"].append((row[0], lower_item))
|
worker["requests"].append(WorkRequests(date=date, shift=lower_item))
|
||||||
|
|
||||||
|
elif lower_item in ("work_request", "happy to work"):
|
||||||
|
worker["requests"].append(WorkRequests(date=date, shift="*"))
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
worker["leave"].append((row[0], lower_item))
|
worker["leave"].append(NotAvailableToWork(date=date, reason=lower_item))
|
||||||
|
|
||||||
n = n + 1
|
n = n + 1
|
||||||
|
|
||||||
|
|||||||
+66
-2
@@ -18,6 +18,14 @@ table {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shift-table {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shift-table thead {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.table-div {
|
.table-div {
|
||||||
/* overflow-x: auto; */
|
/* overflow-x: auto; */
|
||||||
width: 80%;
|
width: 80%;
|
||||||
@@ -107,10 +115,15 @@ th.bank-holiday {
|
|||||||
color: #0074D9;
|
color: #0074D9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torquay {
|
.torquay, .torbay {
|
||||||
color: #5FA8E8;
|
color: #5FA8E8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.taunton {
|
||||||
|
color: #e85fdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.unavailable {
|
.unavailable {
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
}
|
}
|
||||||
@@ -167,7 +180,7 @@ th.bank-holiday {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nwd {
|
.nwd {
|
||||||
color: brown;
|
color: lightcoral;
|
||||||
}
|
}
|
||||||
|
|
||||||
.th {}
|
.th {}
|
||||||
@@ -235,3 +248,54 @@ summary h2 {
|
|||||||
color: red;
|
color: red;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* table.transposed {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed tr {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed th,
|
||||||
|
table.transposed td {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid black;
|
||||||
|
max-width: 100px;
|
||||||
|
} */
|
||||||
|
table.transposed {
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed tr {
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed th,
|
||||||
|
table.transposed td {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: unset;
|
||||||
|
position: relative;
|
||||||
|
border-top: solid 0.5px black;
|
||||||
|
border-right: solid 0.5px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed .Sat {
|
||||||
|
/* opacity: 30%; */
|
||||||
|
/* background-color: lightsteelblue; */
|
||||||
|
border-top: solid 2px lightsteelblue !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed .Sun {
|
||||||
|
border-bottom: solid 2px lightsteelblue !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed .bank-holiday {
|
||||||
|
border-bottom: dotted 2px blue !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table.transposed th.bank-holiday {
|
||||||
|
color: darkblue;
|
||||||
|
|
||||||
|
}
|
||||||
+53
-1
@@ -161,7 +161,7 @@ function generateExtra() {
|
|||||||
<span>Total shifts: ${total_shifts}, </span>
|
<span>Total shifts: ${total_shifts}, </span>
|
||||||
<span>Weekends: ${weekends_worked}, </span>
|
<span>Weekends: ${weekends_worked}, </span>
|
||||||
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
||||||
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff}, </span>
|
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
|
||||||
|
|
||||||
</div>`)
|
</div>`)
|
||||||
|
|
||||||
@@ -377,9 +377,61 @@ function viewWorker(worker) {
|
|||||||
</div>`)
|
</div>`)
|
||||||
$("body").append(dlg)
|
$("body").append(dlg)
|
||||||
|
|
||||||
|
console.log(ds.shiftCounts)
|
||||||
|
shifts = JSON.parse(ds.shiftCounts)
|
||||||
|
shift_targets = JSON.parse(ds.workerTargets)
|
||||||
|
shift_diffs = JSON.parse(ds.shiftDiff)
|
||||||
|
|
||||||
|
let shift_count_table = document.createElement("table")
|
||||||
|
shift_count_table.classList.add("shift-table")
|
||||||
|
let header_row = shift_count_table.createTHead().insertRow();
|
||||||
|
header_row.insertCell().appendChild(document.createTextNode("Shift"))
|
||||||
|
header_row.insertCell().appendChild(document.createTextNode("Count"))
|
||||||
|
header_row.insertCell().appendChild(document.createTextNode("Target"))
|
||||||
|
header_row.insertCell().appendChild(document.createTextNode("Diffs"))
|
||||||
|
|
||||||
|
|
||||||
|
table_body = shift_count_table.createTBody()
|
||||||
|
|
||||||
|
for (const shift in shifts) {
|
||||||
|
let row = table_body.insertRow();
|
||||||
|
row.insertCell().appendChild(document.createTextNode(shift))
|
||||||
|
row.insertCell().appendChild(document.createTextNode(shifts[shift]))
|
||||||
|
row.insertCell().appendChild(document.createTextNode(shift_targets[shift].toPrecision(2)))
|
||||||
|
row.insertCell().appendChild(document.createTextNode(shift_diffs[shift].toPrecision(2)))
|
||||||
|
//console.log(shift, shifts[shift]);
|
||||||
|
}
|
||||||
|
|
||||||
|
dlg.get(0).appendChild(shift_count_table)
|
||||||
dlg.dialog()
|
dlg.dialog()
|
||||||
|
|
||||||
|
|
||||||
console.log(worker);
|
console.log(worker);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$("#export-div").append($("#main-table").clone().attr("id", "gen-table").addClass("transposed"))
|
||||||
|
|
||||||
|
$("#gen-table").each(function() {
|
||||||
|
var $this = $(this);
|
||||||
|
var newrows = [];
|
||||||
|
//$this.find("th.site-title").parent().remove();
|
||||||
|
$this.find("tr").each(function(rowToColIndex){
|
||||||
|
$(this).find("td, th").each(function(colToRowIndex){
|
||||||
|
if(newrows[colToRowIndex] === undefined) { newrows[colToRowIndex] = $("<tr></tr>"); }
|
||||||
|
while(newrows[colToRowIndex].find("td, th").length < rowToColIndex){
|
||||||
|
newrows[colToRowIndex].append($("<td></td>"));
|
||||||
|
}
|
||||||
|
newrows[colToRowIndex].append($(this));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$this.find("tr").remove();
|
||||||
|
$.each(newrows, function(){
|
||||||
|
$this.append(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this.find("td.rota-day").each((n, el) => {
|
||||||
|
console.log(el)
|
||||||
|
$(el).text(el.dataset.shift)
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,3 +4,9 @@ pytest
|
|||||||
black
|
black
|
||||||
rich
|
rich
|
||||||
pydantic
|
pydantic
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
fastapi-sqlalchemy
|
||||||
|
alembic
|
||||||
|
psycopg2
|
||||||
|
python-dotenv
|
||||||
+3
-12
@@ -41,18 +41,12 @@ rota_collections["current"].add_shifts(
|
|||||||
SingleShift(("truro",), "weekend_truro", 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(("torquay",), "weekend_torquay", 12.5, days[5:], assign_as_block=True),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth",),
|
sites=("plymouth",), name="weekend_plymouth", length= 8, days=days[5:],
|
||||||
"weekend_plymouth",
|
|
||||||
8,
|
|
||||||
days[5:],
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
assign_as_block=True,
|
assign_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
(sites),
|
sites=(sites), name="night_weekday", length= 12.25, days=days[:4],
|
||||||
"night_weekday",
|
|
||||||
12.25,
|
|
||||||
days[:4],
|
|
||||||
balance_offset=5,
|
balance_offset=5,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
@@ -61,10 +55,7 @@ rota_collections["current"].add_shifts(
|
|||||||
constraints=["night"],
|
constraints=["night"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
(sites),
|
sites=(sites), name="night_weekend", length= 12.25, days=days[4:],
|
||||||
"night_weekend",
|
|
||||||
12.25,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
|
|||||||
+11
-44
@@ -78,10 +78,7 @@ Rota.constraint_options["require_presence_at_site_overnight"] = ["plymouth"]
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("exeter",),
|
sites=("exeter",), name="exeter_twilight", length= 12.5, days=days[:4],
|
||||||
"exeter_twilight",
|
|
||||||
12.5,
|
|
||||||
days[:4],
|
|
||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
constraints=["max_2_shifts_per_week"],
|
constraints=["max_2_shifts_per_week"],
|
||||||
),
|
),
|
||||||
@@ -89,76 +86,52 @@ Rota.add_shifts(
|
|||||||
("truro", "truro no proc"), "truro_twilight", 12.5, days[:4], balance_offset=3
|
("truro", "truro no proc"), "truro_twilight", 12.5, days[:4], balance_offset=3
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("torbay", "torbay twilights"),
|
sites=("torbay", "torbay twilights"), name="torbay_twilight", length= 12.5, days=days[:4],
|
||||||
"torbay_twilight",
|
|
||||||
12.5,
|
|
||||||
days[:4],
|
|
||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
assign_as_block=True,
|
assign_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth", "plymouth_twilights"),
|
sites=("plymouth", "plymouth_twilights"), name="plymouth_twilight", length= 12.5, days=days[:5],
|
||||||
"plymouth_twilight",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
constraints=["max_2_shifts_per_week"],
|
constraints=["max_2_shifts_per_week"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("exeter",),
|
sites=("exeter",), name="weekend_exeter", length= 12.5, days=days[4:],
|
||||||
"weekend_exeter",
|
|
||||||
12.5,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("truro", "truro no proc"),
|
sites=("truro", "truro no proc"), name="weekend_truro", length= 12.5, days=days[4:],
|
||||||
"weekend_truro",
|
|
||||||
12.5,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
# force_as_block_unless_nwd=True
|
# force_as_block_unless_nwd=True
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("torbay",),
|
sites=("torbay",), name="weekend_torbay", length= 12.5, days=days[4:],
|
||||||
"weekend_torbay",
|
|
||||||
12.5,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
# force_as_block_unless_nwd=True
|
# force_as_block_unless_nwd=True
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth",),
|
sites=("plymouth",), name="weekend_plymouth1", length= 8, days=days[5:],
|
||||||
"weekend_plymouth1",
|
|
||||||
8,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth",),
|
sites=("plymouth",), name="weekend_plymouth2", length= 8, days=days[5:],
|
||||||
"weekend_plymouth2",
|
|
||||||
8,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth", "plymouth_twilights"),
|
sites=("plymouth", "plymouth_twilights"), name="plymouth_bank_holidays", length= 8, days=days[5:],
|
||||||
"plymouth_bank_holidays",
|
|
||||||
8,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=2,
|
balance_offset=2,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
@@ -166,10 +139,7 @@ Rota.add_shifts(
|
|||||||
bank_holidays_only=True,
|
bank_holidays_only=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
(sites[:-1]),
|
sites=(sites[:-1]), name="night_weekday", length= 12.25, days=days[:4],
|
||||||
"night_weekday",
|
|
||||||
12.25,
|
|
||||||
days[:4],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
# hard_constrain_shift=False,
|
# hard_constrain_shift=False,
|
||||||
@@ -179,10 +149,7 @@ Rota.add_shifts(
|
|||||||
constraints=["night"],
|
constraints=["night"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
(sites[:-1]),
|
sites=(sites[:-1]), name="night_weekend", length= 12.25, days=days[4:],
|
||||||
"night_weekend",
|
|
||||||
12.25,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
# hard_constrain_shift=False,
|
# hard_constrain_shift=False,
|
||||||
|
|||||||
+298
-124
@@ -1,9 +1,9 @@
|
|||||||
from calendar import week
|
from calendar import week
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
from typing import Dict, Iterable, List, Sequence, Tuple, Set
|
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type
|
||||||
|
|
||||||
from pydantic import BaseModel, Extra
|
from pydantic import BaseModel, Extra, constr
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -47,6 +47,11 @@ SHIFT_BOUNDS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ShiftConstraint(BaseModel):
|
||||||
|
name: str
|
||||||
|
options: bool | int | Dict | tuple = False
|
||||||
|
|
||||||
|
|
||||||
class SingleShift(BaseModel):
|
class SingleShift(BaseModel):
|
||||||
"""Class to hold all details for a shift
|
"""Class to hold all details for a shift
|
||||||
|
|
||||||
@@ -70,40 +75,39 @@ class SingleShift(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sites: Iterable[str]
|
|
||||||
|
sites: List[str]
|
||||||
name: str
|
name: str
|
||||||
length: float
|
length: float
|
||||||
shift_days: Sequence[str]
|
days: str | List[str]
|
||||||
balance_offset: float | None = None,
|
balance_offset: float | None = None
|
||||||
balance_weighting: float = 1,
|
balance_weighting: float = 1
|
||||||
workers_required: float = 1,
|
workers_required: float = 1
|
||||||
rota_on_nwds: bool = False,
|
rota_on_nwds: bool = False
|
||||||
assign_as_block: bool = False,
|
assign_as_block: bool = False
|
||||||
force_as_block: bool = False,
|
force_as_block: bool = False
|
||||||
force_as_block_unless_nwd: bool = False,
|
force_as_block_unless_nwd: bool = False
|
||||||
hard_constrain_shift: bool = True,
|
hard_constrain_shift: bool = True
|
||||||
bank_holidays_only: bool = False,
|
bank_holidays_only: bool = False
|
||||||
constraint: Iterable[str] = [],
|
constraint: List[ShiftConstraint] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = Extra.allow
|
extra = Extra.allow
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
def __init__(self, **data: Any):
|
def __init__(self, **data: Any):
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
# self.site = sites
|
# self.site = sites
|
||||||
# self.name = name
|
# self.name = name
|
||||||
# self.length = length
|
# self.length = length
|
||||||
#self.shift_days = shift_days
|
# self.days = days
|
||||||
# self.balance_by_site = balance_by_site
|
# self.balance_by_site = balance_by_site
|
||||||
# balance_offset defines the max difference in allocated shifts
|
# balance_offset defines the max difference in allocated shifts
|
||||||
# versus target shifts (hard constraint)
|
# versus target shifts (hard constraint)
|
||||||
# if 0 exactly equal shifts must be assigend (unlikely to be possible)
|
# if 0 exactly equal shifts must be assigend (unlikely to be possible)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if self.balance_offset is None:
|
if self.balance_offset is None:
|
||||||
self.balance_offset = len(self.shift_days)
|
self.balance_offset = len(self.days)
|
||||||
|
|
||||||
|
|
||||||
if self.force_as_block_unless_nwd:
|
if self.force_as_block_unless_nwd:
|
||||||
self.assign_as_block = True
|
self.assign_as_block = True
|
||||||
@@ -113,23 +117,33 @@ class SingleShift(BaseModel):
|
|||||||
self.constraint_options = {}
|
self.constraint_options = {}
|
||||||
self.constraints = []
|
self.constraints = []
|
||||||
for c in self.constraint:
|
for c in self.constraint:
|
||||||
match c:
|
self.constraints.append(c.name)
|
||||||
case (constraint, options):
|
|
||||||
self.constraints.append(constraint)
|
|
||||||
self.constraint_options[constraint] = options
|
|
||||||
case constraint:
|
|
||||||
self.constraints.append(constraint)
|
|
||||||
|
|
||||||
|
if c.options:
|
||||||
|
self.constraint_options[c.name] = c.options
|
||||||
|
|
||||||
|
# constraint = c.constraint
|
||||||
|
# match c:
|
||||||
|
# case (constraint, options):
|
||||||
|
# self.constraint_options[constraint] = options
|
||||||
|
# case constraint:
|
||||||
|
# self.constraints.append(constraint)
|
||||||
|
|
||||||
|
if isinstance(self.days, str):
|
||||||
|
self.days = [self.days]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"name: {self.name}, site: {self.site}, length: {self.length}, balance weighting: {self.balance_weighting}, balance offset: {self.balance_offset}, hard_constrain: {self.hard_constrain_shift}, workers required : {self.workers_required}, rota on nwds {self.rota_on_nwds}, assign as block: {self.assign_as_block}, force as block: {self.force_as_block}, constraints: {self.constraints}, constraint_options: {self.constraint_options}" # , total shifts: {self.total_shifts}"
|
return f"name: {self.name}, site: {self.sites}, length: {self.length}, balance weighting: {self.balance_weighting}, balance offset: {self.balance_offset}, hard_constrain: {self.hard_constrain_shift}, workers required : {self.workers_required}, rota on nwds {self.rota_on_nwds}, assign as block: {self.assign_as_block}, force as block: {self.force_as_block}, constraints: {self.constraints}, constraint_options: {self.constraint_options}" # , total shifts: {self.total_shifts}"
|
||||||
|
|
||||||
def get_shift_summary(self):
|
def get_shift_summary(self):
|
||||||
"""
|
"""
|
||||||
Returns a text summary of the shift
|
Returns a text summary of the shift
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return f"{self.name}: {self.workers_required} workers for sites ({', '.join(self.site)}) on days ({', '.join(self.shift_days)})"
|
return f"{self.name}: {self.workers_required} workers for sites ({', '.join(self.site)}) on days ({', '.join(self.days)})"
|
||||||
|
|
||||||
|
def get_shift_number(self):
|
||||||
|
return len(self.days)
|
||||||
|
|
||||||
|
|
||||||
class RotaBuilder(object):
|
class RotaBuilder(object):
|
||||||
@@ -137,16 +151,14 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
start_date: datetime.datetime = datetime.datetime.now(),
|
start_date: datetime.date = datetime.date.today(),
|
||||||
weeks_to_rota: int = 26,
|
weeks_to_rota: int = 26,
|
||||||
balance_offset_modifier: int = 1,
|
balance_offset_modifier: int = 1,
|
||||||
ltft_balance_offset: int = 1,
|
ltft_balance_offset: int = 1,
|
||||||
max_night_frequency: int = 2,
|
|
||||||
max_weekend_frequency: int = 1, # Requires balance weekends
|
|
||||||
use_previous_shifts: bool = False,
|
use_previous_shifts: bool = False,
|
||||||
use_shift_balance_extra: bool = False,
|
use_shift_balance_extra: bool = False,
|
||||||
use_bank_holiday_extra: bool = False,
|
use_bank_holiday_extra: bool = False,
|
||||||
SHIFT_BOUNDS = SHIFT_BOUNDS
|
SHIFT_BOUNDS=SHIFT_BOUNDS,
|
||||||
):
|
):
|
||||||
|
|
||||||
print(f"Start time: {datetime.datetime.now()}")
|
print(f"Start time: {datetime.datetime.now()}")
|
||||||
@@ -155,7 +167,7 @@ class RotaBuilder(object):
|
|||||||
print(f"Use previous shifts (balance_extra) {use_shift_balance_extra}")
|
print(f"Use previous shifts (balance_extra) {use_shift_balance_extra}")
|
||||||
|
|
||||||
self.SHIFT_BOUNDS = SHIFT_BOUNDS
|
self.SHIFT_BOUNDS = SHIFT_BOUNDS
|
||||||
self.shifts = [] # type List[SingleShift]
|
self.shifts: List[SingleShift] = [] # type List[SingleShift]
|
||||||
|
|
||||||
self.days = days
|
self.days = days
|
||||||
|
|
||||||
@@ -180,7 +192,7 @@ class RotaBuilder(object):
|
|||||||
self.unavailable_to_work_reason = {}
|
self.unavailable_to_work_reason = {}
|
||||||
self.pref_not_to_work = {}
|
self.pref_not_to_work = {}
|
||||||
self.pref_not_to_work_reason = {}
|
self.pref_not_to_work_reason = {}
|
||||||
self.work_requests = set()
|
self.work_requests: set[Tuple[str, WeekInt, DayStr, ShiftName]] = set()
|
||||||
self.work_requests_map = {}
|
self.work_requests_map = {}
|
||||||
|
|
||||||
self.workers: list[Worker] = []
|
self.workers: list[Worker] = []
|
||||||
@@ -193,9 +205,6 @@ class RotaBuilder(object):
|
|||||||
self.balance_offset_modifier = balance_offset_modifier
|
self.balance_offset_modifier = balance_offset_modifier
|
||||||
self.ltft_balance_offset = ltft_balance_offset
|
self.ltft_balance_offset = ltft_balance_offset
|
||||||
|
|
||||||
# Don't assign multiple shifts every (n) weeks
|
|
||||||
self.max_night_frequency = max_night_frequency
|
|
||||||
self.max_weekend_frequency = max_weekend_frequency
|
|
||||||
|
|
||||||
self.constraint_options = {
|
self.constraint_options = {
|
||||||
"balance_nights": True,
|
"balance_nights": True,
|
||||||
@@ -209,6 +218,10 @@ class RotaBuilder(object):
|
|||||||
"minimise_shift_diffs": False, # less sophisticated version of balance_shifts_over_workers
|
"minimise_shift_diffs": False, # less sophisticated version of balance_shifts_over_workers
|
||||||
"balance_weekends": True,
|
"balance_weekends": True,
|
||||||
"max_weekends": 100,
|
"max_weekends": 100,
|
||||||
|
# Don't assign multiple shifts every (n) weeks
|
||||||
|
"max_weekend_frequency": 1, # Requires balance weekends
|
||||||
|
# Don't assign multiple shifts every (n) weeks
|
||||||
|
"max_night_frequency": 4,
|
||||||
"max_shifts_per_week": 7,
|
"max_shifts_per_week": 7,
|
||||||
"max_shifts_per_month": 40,
|
"max_shifts_per_month": 40,
|
||||||
# The following will cause an unsolvable problem if a shift
|
# The following will cause an unsolvable problem if a shift
|
||||||
@@ -219,6 +232,7 @@ class RotaBuilder(object):
|
|||||||
"prevent_thursdays_before_full_weekends": [],
|
"prevent_thursdays_before_full_weekends": [],
|
||||||
"avoid_st2_first_month": False,
|
"avoid_st2_first_month": False,
|
||||||
"hard_constrain_pair_separation": False,
|
"hard_constrain_pair_separation": False,
|
||||||
|
"avoid_shifts_by_grades": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate a map for week, day combinations to a given date
|
# Generate a map for week, day combinations to a given date
|
||||||
@@ -263,7 +277,9 @@ class RotaBuilder(object):
|
|||||||
if not results.solver.status:
|
if not results.solver.status:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def build_and_solve(self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True):
|
def build_and_solve(
|
||||||
|
self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True
|
||||||
|
):
|
||||||
self.build_shifts()
|
self.build_shifts()
|
||||||
self.build_workers()
|
self.build_workers()
|
||||||
self.build_model()
|
self.build_model()
|
||||||
@@ -611,11 +627,22 @@ class RotaBuilder(object):
|
|||||||
initialize=availability_init,
|
initialize=availability_init,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
work_request_sets = set()
|
||||||
|
for work_request in self.work_requests:
|
||||||
|
if work_request[3] == "*":
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
work_request_sets.add(work_request[3])
|
||||||
|
|
||||||
|
|
||||||
# Validate shifts are valid
|
# Validate shifts are valid
|
||||||
work_request_sets = set([i[3] for i in self.work_requests])
|
#work_request_sets = set([i[3] for i in self.work_requests])
|
||||||
invalid_shifts = work_request_sets - set(self.get_shift_names())
|
invalid_shifts = work_request_sets - set(self.get_shift_names())
|
||||||
if invalid_shifts:
|
if invalid_shifts:
|
||||||
raise InvalidShift(f"Invalid worker shift request [shift(s): ${invalid_shifts}]")
|
raise InvalidShift(
|
||||||
|
f"Invalid worker shift request [shift(s): ${invalid_shifts}]"
|
||||||
|
)
|
||||||
|
|
||||||
def work_request_init(model, wid, week, day, shift):
|
def work_request_init(model, wid, week, day, shift):
|
||||||
if (wid, week, day, shift) in self.work_requests:
|
if (wid, week, day, shift) in self.work_requests:
|
||||||
@@ -692,7 +719,6 @@ class RotaBuilder(object):
|
|||||||
# self.model.works[worker.id, week, day, shift]
|
# self.model.works[worker.id, week, day, shift]
|
||||||
# for worker in self.workers if worker.site not in site_required))
|
# for worker in self.workers if worker.site not in site_required))
|
||||||
|
|
||||||
|
|
||||||
# Constraint: total hours worked hours worked
|
# Constraint: total hours worked hours worked
|
||||||
# for worker in self.workers:
|
# for worker in self.workers:
|
||||||
# self.model.constraints.add(
|
# self.model.constraints.add(
|
||||||
@@ -735,8 +761,7 @@ class RotaBuilder(object):
|
|||||||
return Constraint.Skip
|
return Constraint.Skip
|
||||||
return (
|
return (
|
||||||
sum(
|
sum(
|
||||||
model.shift_week_worker_assigned[shift.name, week, w.id]
|
model.shift_week_worker_assigned[shift, week, w.id] for w in workers
|
||||||
for w in workers
|
|
||||||
)
|
)
|
||||||
<= limit
|
<= limit
|
||||||
)
|
)
|
||||||
@@ -754,7 +779,7 @@ class RotaBuilder(object):
|
|||||||
Constraint(
|
Constraint(
|
||||||
[week for week in self.weeks],
|
[week for week in self.weeks],
|
||||||
# [shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
|
# [shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
|
||||||
[shift],
|
[shift.name],
|
||||||
[grade],
|
[grade],
|
||||||
[shift.constraint_options["limit_grade_number"][grade]],
|
[shift.constraint_options["limit_grade_number"][grade]],
|
||||||
rule=limitGradesRule,
|
rule=limitGradesRule,
|
||||||
@@ -769,8 +794,7 @@ class RotaBuilder(object):
|
|||||||
raise ValueError("minimum_grade_number: not enough valid workers")
|
raise ValueError("minimum_grade_number: not enough valid workers")
|
||||||
return (
|
return (
|
||||||
sum(
|
sum(
|
||||||
model.shift_week_worker_assigned[shift.name, week, w.id]
|
model.shift_week_worker_assigned[shift, week, w.id] for w in workers
|
||||||
for w in workers
|
|
||||||
)
|
)
|
||||||
>= limit
|
>= limit
|
||||||
)
|
)
|
||||||
@@ -788,7 +812,7 @@ class RotaBuilder(object):
|
|||||||
Constraint(
|
Constraint(
|
||||||
[week for week in self.weeks],
|
[week for week in self.weeks],
|
||||||
# [shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
|
# [shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
|
||||||
[shift],
|
[shift.name],
|
||||||
[grade],
|
[grade],
|
||||||
[min_required],
|
[min_required],
|
||||||
rule=minimumGradesRule,
|
rule=minimumGradesRule,
|
||||||
@@ -796,7 +820,9 @@ class RotaBuilder(object):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def presenceAtRemoteSiteWeek(model, week, shift, required_site, required_number):
|
def presenceAtRemoteSiteWeek(
|
||||||
|
model, week, shift, required_site, required_number
|
||||||
|
):
|
||||||
required_site_workers = [
|
required_site_workers = [
|
||||||
w for w in self.workers if required_site == w.remote_site
|
w for w in self.workers if required_site == w.remote_site
|
||||||
]
|
]
|
||||||
@@ -809,7 +835,9 @@ class RotaBuilder(object):
|
|||||||
>= required_number
|
>= required_number
|
||||||
)
|
)
|
||||||
|
|
||||||
for shift in self.get_shifts_with_constraint("require_remote_site_presence_week"):
|
for shift in self.get_shifts_with_constraint(
|
||||||
|
"require_remote_site_presence_week"
|
||||||
|
):
|
||||||
site, required_number = shift.constraint_options[
|
site, required_number = shift.constraint_options[
|
||||||
"require_remote_site_presence_week"
|
"require_remote_site_presence_week"
|
||||||
]
|
]
|
||||||
@@ -826,7 +854,9 @@ class RotaBuilder(object):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def presenceAtRemoteSite(model, day, week, shift, required_site, required_number):
|
def presenceAtRemoteSite(
|
||||||
|
model, day, week, shift, required_site, required_number
|
||||||
|
):
|
||||||
required_site_workers = [
|
required_site_workers = [
|
||||||
w for w in self.workers if required_site == w.remote_site
|
w for w in self.workers if required_site == w.remote_site
|
||||||
]
|
]
|
||||||
@@ -836,10 +866,7 @@ class RotaBuilder(object):
|
|||||||
return Constraint.Skip
|
return Constraint.Skip
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sum(
|
sum(model.works[w.id, week, day, shift] for w in required_site_workers)
|
||||||
model.works[w.id, week, day, shift]
|
|
||||||
for w in required_site_workers
|
|
||||||
)
|
|
||||||
>= required_number
|
>= required_number
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -886,7 +913,7 @@ class RotaBuilder(object):
|
|||||||
# )
|
# )
|
||||||
for site in self.sites:
|
for site in self.sites:
|
||||||
for shift in self.get_shifts_with_constraint("balance_across_groups"):
|
for shift in self.get_shifts_with_constraint("balance_across_groups"):
|
||||||
if site in shift.site:
|
if site in shift.sites:
|
||||||
block = shift.name
|
block = shift.name
|
||||||
for week in self.weeks:
|
for week in self.weeks:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
@@ -919,7 +946,7 @@ class RotaBuilder(object):
|
|||||||
]
|
]
|
||||||
>= sum(
|
>= sum(
|
||||||
self.model.works[worker.id, week, day, shift_name]
|
self.model.works[worker.id, week, day, shift_name]
|
||||||
for day in shift.shift_days
|
for day in shift.days
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
@@ -941,8 +968,12 @@ class RotaBuilder(object):
|
|||||||
for w in workers:
|
for w in workers:
|
||||||
if w.non_working_day_list:
|
if w.non_working_day_list:
|
||||||
l = []
|
l = []
|
||||||
for nwd, start_nwd_date, end_nwd_date in w.non_working_day_list:
|
for (
|
||||||
if nwd in shift.shift_days:
|
nwd,
|
||||||
|
start_nwd_date,
|
||||||
|
end_nwd_date,
|
||||||
|
) in w.non_working_day_list:
|
||||||
|
if nwd in shift.days:
|
||||||
if start_nwd_date > self.get_week_start_date(week):
|
if start_nwd_date > self.get_week_start_date(week):
|
||||||
continue
|
continue
|
||||||
if end_nwd_date < self.get_week_start_date(week):
|
if end_nwd_date < self.get_week_start_date(week):
|
||||||
@@ -957,8 +988,6 @@ class RotaBuilder(object):
|
|||||||
else:
|
else:
|
||||||
full_workers.append(w)
|
full_workers.append(w)
|
||||||
|
|
||||||
print(nwd_workers)
|
|
||||||
|
|
||||||
if not nwd_workers: # Just do the usual
|
if not nwd_workers: # Just do the usual
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
sum(
|
sum(
|
||||||
@@ -1015,6 +1044,33 @@ class RotaBuilder(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]:
|
||||||
|
if invalid_shifts := set(constraint_dict["shifts"]).difference(
|
||||||
|
set(self.get_shift_names())
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}"
|
||||||
|
)
|
||||||
|
if invalid_grades := set(constraint_dict["grades"]).difference(
|
||||||
|
set(self.get_worker_grades())
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"avoid shifts by grade constraint contains a non existent worker grade: {invalid_grades}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if worker.grade in constraint_dict["grades"]:
|
||||||
|
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 constraint_dict["weeks"]
|
||||||
|
and shift in constraint_dict["shifts"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# sys.exit(0)
|
||||||
|
|
||||||
if self.constraint_options["avoid_st2_first_month"] and worker.grade == 2:
|
if self.constraint_options["avoid_st2_first_month"] and worker.grade == 2:
|
||||||
# Avoid ST1s on the first month
|
# Avoid ST1s on the first month
|
||||||
try:
|
try:
|
||||||
@@ -1049,15 +1105,17 @@ class RotaBuilder(object):
|
|||||||
# no solution will be possible
|
# no solution will be possible
|
||||||
for shift in self.get_shifts():
|
for shift in self.get_shifts():
|
||||||
if (
|
if (
|
||||||
worker.site in shift.site
|
worker.site in shift.sites
|
||||||
): # Each site specfies which sites self.workers can fullfill it
|
): # Each site specfies which sites self.workers can fullfill it
|
||||||
|
|
||||||
# calaculate the total number of shifts that need assigning over the rota
|
# calaculate the total number of shifts that need assigning over the rota
|
||||||
# total_shifts = (len(self.weeks) * len(shift.shift_days) *
|
# total_shifts = (len(self.weeks) * len(shift.days) *
|
||||||
total_shifts = self.shift_counts[shift] * shift.workers_required
|
total_shifts = (
|
||||||
|
self.shift_counts[shift.name] * shift.workers_required
|
||||||
|
)
|
||||||
|
|
||||||
full_time_equivalent_joined = sum(
|
full_time_equivalent_joined = sum(
|
||||||
self.full_time_equivalent_sites[i] for i in shift.site
|
self.full_time_equivalent_sites[i] for i in shift.sites
|
||||||
)
|
)
|
||||||
|
|
||||||
target_shifts = (
|
target_shifts = (
|
||||||
@@ -1111,6 +1169,8 @@ class RotaBuilder(object):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Make sure shifts aren't assigned to those from other sites
|
# Make sure shifts aren't assigned to those from other sites
|
||||||
|
# Not needed if there are no shifts
|
||||||
|
if self.get_week_day_combinations_for_shift(shift):
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
0
|
0
|
||||||
== sum(
|
== sum(
|
||||||
@@ -1147,7 +1207,7 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
if self.constraint_options["balance_shifts_quadratic"]:
|
if self.constraint_options["balance_shifts_quadratic"]:
|
||||||
# This may need to be updated
|
# This may need to be updated
|
||||||
xU = 10
|
xU = 25
|
||||||
xL = 1
|
xL = 1
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
inequality(
|
inequality(
|
||||||
@@ -1304,9 +1364,9 @@ class RotaBuilder(object):
|
|||||||
# We use a similar method to balance the number of weekends worked
|
# We use a similar method to balance the number of weekends worked
|
||||||
# This works as long as weekend shifts are assigned as blocks!
|
# This works as long as weekend shifts are assigned as blocks!
|
||||||
weekend_shift_target_number = sum(
|
weekend_shift_target_number = sum(
|
||||||
worker.shift_target_number[shift.name] / len(shift.shift_days)
|
worker.shift_target_number[shift.name] / len(shift.days)
|
||||||
for shift in self.get_shifts()
|
for shift in self.get_shifts()
|
||||||
if "Sat" in shift.shift_days or "Sun" in shift.shift_days
|
if "Sat" in shift.days or "Sun" in shift.days
|
||||||
)
|
)
|
||||||
|
|
||||||
worker.weekend_shift_target_number = weekend_shift_target_number
|
worker.weekend_shift_target_number = weekend_shift_target_number
|
||||||
@@ -1374,7 +1434,7 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
if self.constraint_options["balance_blocks"]:
|
if self.constraint_options["balance_blocks"]:
|
||||||
for week_blocks in self.get_week_block_iterator(
|
for week_blocks in self.get_week_block_iterator(
|
||||||
self.max_night_frequency
|
self.constraint_options["max_night_frequency"]
|
||||||
):
|
):
|
||||||
if self.get_shifts_with_constraint("night"):
|
if self.get_shifts_with_constraint("night"):
|
||||||
# Prevent nights more than once every n weeks
|
# Prevent nights more than once every n weeks
|
||||||
@@ -1390,7 +1450,7 @@ class RotaBuilder(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# if self.constraint_options["balance_weekends"]:
|
# if self.constraint_options["balance_weekends"]:
|
||||||
for week_blocks in self.get_week_block_iterator(self.max_weekend_frequency):
|
for week_blocks in self.get_week_block_iterator(self.constraint_options["max_weekend_frequency"]):
|
||||||
# Prevent weekend shifts more than once every n weeks
|
# Prevent weekend shifts more than once every n weeks
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
1
|
1
|
||||||
@@ -1400,12 +1460,55 @@ class RotaBuilder(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for constraint_shift in self.get_shifts_with_constraints(
|
||||||
|
"max_shifts_per_week_block"
|
||||||
|
):
|
||||||
|
constraint_options = constraint_shift.constraint_options[
|
||||||
|
"max_shifts_per_week_block"
|
||||||
|
]
|
||||||
|
|
||||||
|
# If not supplied, default to the number of days per shift
|
||||||
|
shift_number = constraint_shift.get_shift_number()
|
||||||
|
if "shift_number" in constraint_options:
|
||||||
|
shift_number = constraint_options["shift_number"]
|
||||||
|
|
||||||
|
for week_blocks in self.get_week_block_iterator(
|
||||||
|
constraint_options["weeks"]
|
||||||
|
):
|
||||||
|
# Prevent shifts more than once every n weeks
|
||||||
|
self.model.constraints.add(
|
||||||
|
shift_number
|
||||||
|
>= sum(
|
||||||
|
self.model.works[
|
||||||
|
worker.id, week, day, constraint_shift.name
|
||||||
|
]
|
||||||
|
for week in week_blocks
|
||||||
|
for day in constraint_shift.days
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for week in self.weeks:
|
for week in self.weeks:
|
||||||
|
for constraint_shift in self.get_shifts_with_constraints(
|
||||||
|
"max_shifts_per_week"
|
||||||
|
):
|
||||||
|
self.model.constraints.add(
|
||||||
|
constraint_shift.constraint_options["max_shifts_per_week"]
|
||||||
|
>= sum(
|
||||||
|
self.model.works[worker.id, w, day, constraint_shift.name]
|
||||||
|
# for shiftname in self.get_shift_names_by_week_day(week, day)
|
||||||
|
# for day in self.days
|
||||||
|
for w, day in self.get_week_day_combinations_for_shift(
|
||||||
|
constraint_shift
|
||||||
|
)
|
||||||
|
if w == week
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
self.constraint_options["max_shifts_per_week"]
|
self.constraint_options["max_shifts_per_week"]
|
||||||
>= sum(
|
>= sum(
|
||||||
self.model.works[worker.id, week, day, shiftname]
|
self.model.works[worker.id, w, day, shiftname]
|
||||||
# for shiftname in self.get_shift_names_by_week_day(week, day)
|
# for shiftname in self.get_shift_names_by_week_day(week, day)
|
||||||
# for day in self.days
|
# for day in self.days
|
||||||
for w, day, shiftname in self.get_all_shiftname_combinations(
|
for w, day, shiftname in self.get_all_shiftname_combinations(
|
||||||
@@ -1485,21 +1588,15 @@ class RotaBuilder(object):
|
|||||||
full_weekend_count
|
full_weekend_count
|
||||||
>= sum(
|
>= sum(
|
||||||
self.model.works[worker.id, week, "Sat", shift.name]
|
self.model.works[worker.id, week, "Sat", shift.name]
|
||||||
for shift in self.get_shift_names_by_week_day(
|
for shift in self.get_shift_names_by_week_day(week, "Sat")
|
||||||
week, "Sat"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
+ sum(
|
+ sum(
|
||||||
self.model.works[worker.id, week, "Sun", shift.name]
|
self.model.works[worker.id, week, "Sun", shift.name]
|
||||||
for shift in self.get_shift_names_by_week_day(
|
for shift in self.get_shift_names_by_week_day(week, "Sat")
|
||||||
week, "Sat"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
+ sum(
|
+ sum(
|
||||||
self.model.works[worker.id, week, "Fri", shift.name]
|
self.model.works[worker.id, week, "Fri", shift.name]
|
||||||
for shift in self.get_shift_names_by_week_day(
|
for shift in self.get_shift_names_by_week_day(week, "Sat")
|
||||||
week, "Sat"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
@@ -1510,21 +1607,15 @@ class RotaBuilder(object):
|
|||||||
full_weekend_count
|
full_weekend_count
|
||||||
>= sum(
|
>= sum(
|
||||||
self.model.works[worker.id, week, "Sat", shift.name]
|
self.model.works[worker.id, week, "Sat", shift.name]
|
||||||
for shift in self.get_shift_names_by_week_day(
|
for shift in self.get_shift_names_by_week_day(week, "Sat")
|
||||||
week, "Sat"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
+ sum(
|
+ sum(
|
||||||
self.model.works[worker.id, week, "Sun", shift.name]
|
self.model.works[worker.id, week, "Sun", shift.name]
|
||||||
for shift in self.get_shift_names_by_week_day(
|
for shift in self.get_shift_names_by_week_day(week, "Sun")
|
||||||
week, "Sun"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
+ sum(
|
+ sum(
|
||||||
self.model.works[worker.id, week, "Thu", shift.name]
|
self.model.works[worker.id, week, "Thu", shift.name]
|
||||||
for shift in self.get_shift_names_by_week_day(
|
for shift in self.get_shift_names_by_week_day(week, "Thu")
|
||||||
week, "Thu"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1576,6 +1667,27 @@ class RotaBuilder(object):
|
|||||||
# self.model.shift_week_worker_assigned[s.name, week, worker.id]
|
# self.model.shift_week_worker_assigned[s.name, week, worker.id]
|
||||||
# for s in self.get_shifts_with_constraint("night")))
|
# for s in self.get_shifts_with_constraint("night")))
|
||||||
|
|
||||||
|
#for shift in self.get_shifts_with_constraint("single_block_per_week"):
|
||||||
|
# if shift.force_as_block:
|
||||||
|
# # Force nights to be assigned in blocks
|
||||||
|
# # self.model.constraints.add(8* self.model.shift_week_worker_assigned[shift.name, week, worker.id] >= sum(self.model.works[worker.id, week, day, shift.name] for day in self.days)
|
||||||
|
# # )
|
||||||
|
|
||||||
|
# # Force night shifts to be assigned in blocks
|
||||||
|
# self.model.constraints.add(
|
||||||
|
# self.model.shift_week_worker_assigned[
|
||||||
|
# shift.name, week, worker.id
|
||||||
|
# ]
|
||||||
|
# * len(shift.days)
|
||||||
|
# == sum(
|
||||||
|
# self.model.works[worker.id, w, d, s]
|
||||||
|
# for w, d, s in self.get_all_shiftname_combinations(week=week)
|
||||||
|
# #for day in self.days
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# raise ValueError(f"Requires {shift.name} to have force_as_block")
|
||||||
|
|
||||||
# for shift in self.get_shifts_with_constraint("night"):
|
# for shift in self.get_shifts_with_constraint("night"):
|
||||||
for shift in self.get_shifts():
|
for shift in self.get_shifts():
|
||||||
if shift.force_as_block:
|
if shift.force_as_block:
|
||||||
@@ -1588,10 +1700,10 @@ class RotaBuilder(object):
|
|||||||
self.model.shift_week_worker_assigned[
|
self.model.shift_week_worker_assigned[
|
||||||
shift.name, week, worker.id
|
shift.name, week, worker.id
|
||||||
]
|
]
|
||||||
* len(shift.shift_days)
|
* len(shift.days)
|
||||||
== sum(
|
== sum(
|
||||||
self.model.works[worker.id, week, day, shift.name]
|
self.model.works[worker.id, week, day, shift.name]
|
||||||
for day in shift.shift_days
|
for day in shift.days
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1603,6 +1715,9 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
p1 = 1
|
p1 = 1
|
||||||
p2 = 1
|
p2 = 1
|
||||||
|
p3 = 1
|
||||||
|
p4 = 1
|
||||||
|
p5 = 1
|
||||||
if n > 0:
|
if n > 0:
|
||||||
pweek, pday = weeks_days[n - 1]
|
pweek, pday = weeks_days[n - 1]
|
||||||
else:
|
else:
|
||||||
@@ -1615,6 +1730,24 @@ class RotaBuilder(object):
|
|||||||
p2week, p2day = weeks_days[n]
|
p2week, p2day = weeks_days[n]
|
||||||
p2 = 0
|
p2 = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
p3week, p3day = weeks_days[n - 3]
|
||||||
|
except IndexError:
|
||||||
|
p3week, p3day = weeks_days[n]
|
||||||
|
p3 = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
p4week, p4day = weeks_days[n - 4]
|
||||||
|
except IndexError:
|
||||||
|
p4week, p4day = weeks_days[n]
|
||||||
|
p4 = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
p5week, p5day = weeks_days[n - 5]
|
||||||
|
except IndexError:
|
||||||
|
p5week, p5day = weeks_days[n]
|
||||||
|
p5 = 0
|
||||||
|
|
||||||
n1 = 1
|
n1 = 1
|
||||||
n2 = 1
|
n2 = 1
|
||||||
try:
|
try:
|
||||||
@@ -1679,7 +1812,7 @@ class RotaBuilder(object):
|
|||||||
for constraint_shift in self.get_shifts_with_constraints(
|
for constraint_shift in self.get_shifts_with_constraints(
|
||||||
"preclear", "preclear2"
|
"preclear", "preclear2"
|
||||||
):
|
):
|
||||||
if day in constraint_shift.shift_days:
|
if day in constraint_shift.days:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
1
|
1
|
||||||
>= self.model.works[
|
>= self.model.works[
|
||||||
@@ -1696,7 +1829,7 @@ class RotaBuilder(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for constraint_shift in self.get_shifts_with_constraint("preclear2"):
|
for constraint_shift in self.get_shifts_with_constraint("preclear2"):
|
||||||
if day in constraint_shift.shift_days:
|
if day in constraint_shift.days:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
1
|
1
|
||||||
>= self.model.works[
|
>= self.model.works[
|
||||||
@@ -1715,7 +1848,7 @@ class RotaBuilder(object):
|
|||||||
for constraint_shift in self.get_shifts_with_constraints(
|
for constraint_shift in self.get_shifts_with_constraints(
|
||||||
"postclear", "postclear2"
|
"postclear", "postclear2"
|
||||||
):
|
):
|
||||||
if day in constraint_shift.shift_days:
|
if day in constraint_shift.days:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
1
|
1
|
||||||
>= self.model.works[
|
>= self.model.works[
|
||||||
@@ -1732,7 +1865,7 @@ class RotaBuilder(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for constraint_shift in self.get_shifts_with_constraints("postclear2"):
|
for constraint_shift in self.get_shifts_with_constraints("postclear2"):
|
||||||
if day in constraint_shift.shift_days:
|
if day in constraint_shift.days:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
1
|
1
|
||||||
>= self.model.works[
|
>= self.model.works[
|
||||||
@@ -1967,7 +2100,6 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
Must be called prior to attempting to solve
|
Must be called prior to attempting to solve
|
||||||
"""
|
"""
|
||||||
# self.build_shifts()
|
|
||||||
if not self.workers:
|
if not self.workers:
|
||||||
raise NoWorkers("Workers must be added prior to calling build_workers")
|
raise NoWorkers("Workers must be added prior to calling build_workers")
|
||||||
|
|
||||||
@@ -1982,11 +2114,11 @@ class RotaBuilder(object):
|
|||||||
self.workers_id_map[wid] = worker
|
self.workers_id_map[wid] = worker
|
||||||
|
|
||||||
if worker.name in self.workers_name_map:
|
if worker.name in self.workers_name_map:
|
||||||
raise ValueError(f"Worker with name '{worker.name}' has been added twice")
|
raise ValueError(
|
||||||
|
f"Worker with name '{worker.name}' has been added twice"
|
||||||
|
)
|
||||||
self.workers_name_map[worker.name] = worker
|
self.workers_name_map[worker.name] = worker
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.workers = sorted(self.workers)
|
self.workers = sorted(self.workers)
|
||||||
|
|
||||||
self.full_time_equivalent = sum(w.fte_adj for w in self.workers)
|
self.full_time_equivalent = sum(w.fte_adj for w in self.workers)
|
||||||
@@ -2007,7 +2139,14 @@ class RotaBuilder(object):
|
|||||||
for p in pairs:
|
for p in pairs:
|
||||||
self.worker_pairs.append(tuple(pairs[p]))
|
self.worker_pairs.append(tuple(pairs[p]))
|
||||||
|
|
||||||
def add_shift(self, shift) -> None:
|
def add_grade_constraint_by_week(
|
||||||
|
self, grades: Iterable[int], weeks: Iterable[int], shifts
|
||||||
|
):
|
||||||
|
self.constraint_options["avoid_shifts_by_grades"].append(
|
||||||
|
{"grades": grades, "weeks": weeks, "shifts": shifts}
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_shift(self, shift: SingleShift) -> None:
|
||||||
"""Add a shift to the collection
|
"""Add a shift to the collection
|
||||||
|
|
||||||
:param SingleShift shift: Shift object
|
:param SingleShift shift: Shift object
|
||||||
@@ -2023,7 +2162,7 @@ class RotaBuilder(object):
|
|||||||
"""
|
"""
|
||||||
self.shifts.extend(shifts)
|
self.shifts.extend(shifts)
|
||||||
|
|
||||||
def get_shift_names_by_week_day(self, week, day: DayStr) -> set():
|
def get_shift_names_by_week_day(self, week, day: DayStr) -> set:
|
||||||
"""Returns the shifts required for a specific day
|
"""Returns the shifts required for a specific day
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -2066,6 +2205,9 @@ class RotaBuilder(object):
|
|||||||
self.week_day_shift_product = []
|
self.week_day_shift_product = []
|
||||||
self.week_day_shiftclass_product = []
|
self.week_day_shiftclass_product = []
|
||||||
|
|
||||||
|
for s in self.shifts:
|
||||||
|
pass
|
||||||
|
|
||||||
for s in self.shifts:
|
for s in self.shifts:
|
||||||
if s.name in self.shift_names:
|
if s.name in self.shift_names:
|
||||||
raise InvalidShift(f"Duplicate shift: {s.name}")
|
raise InvalidShift(f"Duplicate shift: {s.name}")
|
||||||
@@ -2073,10 +2215,10 @@ class RotaBuilder(object):
|
|||||||
self.shifts_by_name[s.name] = s
|
self.shifts_by_name[s.name] = s
|
||||||
self.shift_names.append(s.name)
|
self.shift_names.append(s.name)
|
||||||
|
|
||||||
# for day in s.shift_days:
|
# for day in s.days:
|
||||||
# self.week_day_shifts_dict[(week, day)].add(s.name)
|
# self.week_day_shifts_dict[(week, day)].add(s.name)
|
||||||
|
|
||||||
for site in s.site:
|
for site in list(s.sites):
|
||||||
self.sites.add(site)
|
self.sites.add(site)
|
||||||
|
|
||||||
self.shift_counts = defaultdict(int)
|
self.shift_counts = defaultdict(int)
|
||||||
@@ -2085,16 +2227,17 @@ class RotaBuilder(object):
|
|||||||
for s in self.shifts:
|
for s in self.shifts:
|
||||||
if s.bank_holidays_only:
|
if s.bank_holidays_only:
|
||||||
if self.week_day_date_map[(week, day)] in bank_holiday_map:
|
if self.week_day_date_map[(week, day)] in bank_holiday_map:
|
||||||
|
print(s.name, self.week_day_date_map[(week, day)])
|
||||||
self.week_day_shift_product.append((week, day, s.name))
|
self.week_day_shift_product.append((week, day, s.name))
|
||||||
self.week_day_shiftclass_product.append((week, day, s))
|
self.week_day_shiftclass_product.append((week, day, s))
|
||||||
self.week_day_shifts_dict[(week, day)].add(s.name)
|
self.week_day_shifts_dict[(week, day)].add(s.name)
|
||||||
self.shift_counts[s] = self.shift_counts[s] + 1
|
self.shift_counts[s.name] = self.shift_counts[s.name] + 1
|
||||||
else:
|
else:
|
||||||
if day in s.shift_days:
|
if day in s.days:
|
||||||
self.week_day_shift_product.append((week, day, s.name))
|
self.week_day_shift_product.append((week, day, s.name))
|
||||||
self.week_day_shiftclass_product.append((week, day, s))
|
self.week_day_shiftclass_product.append((week, day, s))
|
||||||
self.week_day_shifts_dict[(week, day)].add(s.name)
|
self.week_day_shifts_dict[(week, day)].add(s.name)
|
||||||
self.shift_counts[s] = self.shift_counts[s] + 1
|
self.shift_counts[s.name] = self.shift_counts[s.name] + 1
|
||||||
# print(self.week_day_shift_product)
|
# print(self.week_day_shift_product)
|
||||||
|
|
||||||
# # Todo replace with week_day.....
|
# # Todo replace with week_day.....
|
||||||
@@ -2102,7 +2245,7 @@ class RotaBuilder(object):
|
|||||||
# self.day_shiftclass_product = []
|
# self.day_shiftclass_product = []
|
||||||
# for s in self.shifts:
|
# for s in self.shifts:
|
||||||
# for day in days:
|
# for day in days:
|
||||||
# if day in s.shift_days:
|
# if day in s.days:
|
||||||
# self.day_shift_product.append((day, s.name))
|
# self.day_shift_product.append((day, s.name))
|
||||||
# self.day_shiftclass_product.append((day, s))
|
# self.day_shiftclass_product.append((day, s))
|
||||||
#
|
#
|
||||||
@@ -2193,17 +2336,27 @@ class RotaBuilder(object):
|
|||||||
"""
|
"""
|
||||||
return self.shifts
|
return self.shifts
|
||||||
|
|
||||||
|
def get_shifts_for_worker_site(self, worker_site):
|
||||||
|
shifts = [shift for shift in self.shifts if worker_site in shift.sites]
|
||||||
|
return shifts
|
||||||
|
|
||||||
|
def get_shifts_for_worker(self, worker_id):
|
||||||
|
worker = self.get_worker_by_id(worker_id)
|
||||||
|
shifts = [shift for shift in self.shifts if worker.site in shift.sites]
|
||||||
|
return shifts
|
||||||
|
|
||||||
def get_shifts_with_constraint(self, constraint) -> List[SingleShift]:
|
def get_shifts_with_constraint(self, constraint) -> List[SingleShift]:
|
||||||
return [shift for shift in self.shifts if constraint in shift.constraints]
|
return [shift for shift in self.shifts if constraint in shift.constraints]
|
||||||
|
|
||||||
def get_shifts_with_constraints(self, *constraints) -> set[SingleShift]:
|
def get_shifts_with_constraints(self, *constraints) -> List[SingleShift]:
|
||||||
shifts = set()
|
shift_names = set()
|
||||||
|
|
||||||
for constraint in constraints:
|
for constraint in constraints:
|
||||||
shifts.update(
|
shift_names.update(
|
||||||
[shift for shift in self.shifts if constraint in shift.constraints]
|
[shift.name for shift in self.shifts if constraint in shift.constraints]
|
||||||
)
|
)
|
||||||
return shifts
|
|
||||||
|
return [self.get_shift_by_name(s) for s in shift_names]
|
||||||
|
|
||||||
def get_shift_names(self) -> List[ShiftName]:
|
def get_shift_names(self) -> List[ShiftName]:
|
||||||
"""Returns a list of all the registered shift names
|
"""Returns a list of all the registered shift names
|
||||||
@@ -2239,12 +2392,12 @@ class RotaBuilder(object):
|
|||||||
"""Returns a list of all possible shifts, the workers required and site
|
"""Returns a list of all possible shifts, the workers required and site
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: list (week, day, shift name, workers required, shift site)
|
list: list (week, day, shift name, workers required, shift.sites)
|
||||||
"""
|
"""
|
||||||
l = []
|
l = []
|
||||||
for week, day, shift in self.week_day_shiftclass_product:
|
for week, day, shift in self.week_day_shiftclass_product:
|
||||||
# if day in shift.shift_days:
|
# if day in shift.days:
|
||||||
l.append((week, day, shift.name, shift.workers_required, shift.site))
|
l.append((week, day, shift.name, shift.workers_required, shift.sites))
|
||||||
|
|
||||||
return l
|
return l
|
||||||
|
|
||||||
@@ -2265,17 +2418,24 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
return l - set(self.get_all_shiftclass_combinations())
|
return l - set(self.get_all_shiftclass_combinations())
|
||||||
|
|
||||||
def shifts_to_assign_as_blocks(self):
|
def shifts_to_assign_as_blocks(self) -> List[str]:
|
||||||
return [shift.name for shift in self.get_shifts() if shift.assign_as_block]
|
return [shift.name for shift in self.get_shifts() if shift.assign_as_block]
|
||||||
|
|
||||||
def shifts_to_force_as_blocks(self):
|
def shifts_to_force_as_blocks(self) -> List[str]:
|
||||||
return [shift.name for shift in self.get_shifts() if (shift.force_as_block or shift.force_as_block_unless_nwd)]
|
return [
|
||||||
|
shift.name
|
||||||
|
for shift in self.get_shifts()
|
||||||
|
if (shift.force_as_block or shift.force_as_block_unless_nwd)
|
||||||
|
]
|
||||||
|
|
||||||
def shifts_to_assign_or_force_as_blocks(self):
|
def shifts_to_assign_or_force_as_blocks(self) -> List[str]:
|
||||||
s = self.shifts_to_assign_as_blocks()
|
s = self.shifts_to_assign_as_blocks()
|
||||||
s.extend(self.shifts_to_force_as_blocks())
|
s.extend(self.shifts_to_force_as_blocks())
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
def get_worker_by_id(self, worker_id: str) -> Worker:
|
||||||
|
return self.workers_id_map[worker_id]
|
||||||
|
|
||||||
def get_worker_by_name(self, name: str) -> Worker:
|
def get_worker_by_name(self, name: str) -> Worker:
|
||||||
return self.workers_name_map[name]
|
return self.workers_name_map[name]
|
||||||
|
|
||||||
@@ -2301,7 +2461,7 @@ class RotaBuilder(object):
|
|||||||
return group_workers
|
return group_workers
|
||||||
|
|
||||||
def get_workers_for_shift(self, shift: SingleShift) -> List[Worker]:
|
def get_workers_for_shift(self, shift: SingleShift) -> List[Worker]:
|
||||||
return [worker for worker in self.workers if worker.site in shift.site]
|
return [worker for worker in self.workers if worker.site in shift.sites]
|
||||||
|
|
||||||
def get_workers_total_fte(self) -> float:
|
def get_workers_total_fte(self) -> float:
|
||||||
return self.full_time_equivalent
|
return self.full_time_equivalent
|
||||||
@@ -2322,6 +2482,9 @@ class RotaBuilder(object):
|
|||||||
def get_workers(self):
|
def get_workers(self):
|
||||||
return self.workers
|
return self.workers
|
||||||
|
|
||||||
|
def get_worker_grades(self) -> set[int]:
|
||||||
|
return set([worker.grade for worker in self.workers])
|
||||||
|
|
||||||
def get_bank_holiday_week_days(self):
|
def get_bank_holiday_week_days(self):
|
||||||
return [
|
return [
|
||||||
(week, day)
|
(week, day)
|
||||||
@@ -2385,7 +2548,6 @@ class RotaBuilder(object):
|
|||||||
week_table[week][day][shift].append(worker.get_details())
|
week_table[week][day][shift].append(worker.get_details())
|
||||||
return week_table
|
return week_table
|
||||||
|
|
||||||
|
|
||||||
def get_worker_timetable(self):
|
def get_worker_timetable(self):
|
||||||
works = self.model.works
|
works = self.model.works
|
||||||
timetable = {
|
timetable = {
|
||||||
@@ -2481,7 +2643,9 @@ class RotaBuilder(object):
|
|||||||
if d in bank_holiday_map:
|
if d in bank_holiday_map:
|
||||||
th_class = "bank-holiday"
|
th_class = "bank-holiday"
|
||||||
|
|
||||||
date_row.append(f"<th title='{d}' class='{th_class}' data-date='{d}'>Week {week}: {day}</th>")
|
date_row.append(
|
||||||
|
f"<th title='{d}' class='{th_class}' data-date='{d}'>Week {week}: {day}</th>"
|
||||||
|
)
|
||||||
n = n + 1
|
n = n + 1
|
||||||
timetable.append(f"<tr class='data-row'>{''.join(date_row)}</tr>")
|
timetable.append(f"<tr class='data-row'>{''.join(date_row)}</tr>")
|
||||||
|
|
||||||
@@ -2667,7 +2831,7 @@ class RotaBuilder(object):
|
|||||||
html = f"""
|
html = f"""
|
||||||
<body>
|
<body>
|
||||||
<div class="table-div" id="{table_name}">
|
<div class="table-div" id="{table_name}">
|
||||||
<table>{joined_timetable}</table>
|
<table id="main-table">{joined_timetable}</table>
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary><h2>Rota settings</h2></summary>
|
<summary><h2>Rota settings</h2></summary>
|
||||||
@@ -2688,7 +2852,14 @@ class RotaBuilder(object):
|
|||||||
<pre>
|
<pre>
|
||||||
{result_string}
|
{result_string}
|
||||||
</pre>
|
</pre>
|
||||||
|
</details>
|
||||||
<div>
|
<div>
|
||||||
|
<details>
|
||||||
|
<summary><h2>Export table</h2></summary>
|
||||||
|
<div id="export-div">
|
||||||
|
<div>
|
||||||
|
</details>
|
||||||
|
</div
|
||||||
</body>
|
</body>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -2738,12 +2909,12 @@ class RotaBuilder(object):
|
|||||||
shifts = {}
|
shifts = {}
|
||||||
|
|
||||||
for worker in self.workers:
|
for worker in self.workers:
|
||||||
shifts[worker.name] = len([i for i in self.get_worker_shift_list(worker) if i != ""])
|
shifts[worker.name] = len(
|
||||||
|
[i for i in self.get_worker_shift_list(worker) if i != ""]
|
||||||
|
)
|
||||||
|
|
||||||
return shifts
|
return shifts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_worker_shift_list(self, worker: Worker) -> List:
|
def get_worker_shift_list(self, worker: Worker) -> List:
|
||||||
shifts = []
|
shifts = []
|
||||||
|
|
||||||
@@ -2822,14 +2993,17 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
class NoActiveSites(Exception):
|
class NoActiveSites(Exception):
|
||||||
"""Raised when there are no active sites"""
|
"""Raised when there are no active sites"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NoWorkers(Exception):
|
class NoWorkers(Exception):
|
||||||
"""Raised when there are no active sites"""
|
"""Raised when there are no active sites"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidShift(Exception):
|
class InvalidShift(Exception):
|
||||||
"""Raised when there are no active sites"""
|
"""Raised when there are no active sites"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
+43
-18
@@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Iterable, List, Literal, Optional
|
from typing import Iterable, List, Literal, Optional
|
||||||
from pydantic import BaseModel, Extra
|
from pydantic import BaseModel, Extra, validator
|
||||||
|
|
||||||
from rich.pretty import pprint
|
from rich.pretty import pprint
|
||||||
|
|
||||||
@@ -15,29 +15,51 @@ if TYPE_CHECKING:
|
|||||||
from rota.shifts import RotaBuilder
|
from rota.shifts import RotaBuilder
|
||||||
|
|
||||||
days = Literal["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
days = Literal["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||||
|
whole_days = [
|
||||||
|
"sunday",
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class NotAvailableToWork(BaseModel):
|
class NotAvailableToWork(BaseModel):
|
||||||
date: datetime.date
|
date: datetime.date
|
||||||
reason: str = "unknown"
|
reason: str = "unknown"
|
||||||
|
|
||||||
|
|
||||||
class NonWorkingDays(BaseModel):
|
class NonWorkingDays(BaseModel):
|
||||||
day: days
|
day: days
|
||||||
start_date: datetime.date | None = None
|
start_date: datetime.date | None = None
|
||||||
end_date: datetime.date | None = None
|
end_date: datetime.date | None = None
|
||||||
|
|
||||||
|
@validator("day")
|
||||||
|
def day_in(cls, day):
|
||||||
|
for whole_day in whole_days:
|
||||||
|
if day.lower() in whole_day:
|
||||||
|
return whole_day[:3].capitalize()
|
||||||
|
raise ValueError("Invalid day")
|
||||||
|
|
||||||
|
|
||||||
class WorkRequests(BaseModel):
|
class WorkRequests(BaseModel):
|
||||||
date: datetime.date
|
date: datetime.date
|
||||||
shift: str = ""
|
shift: str = ""
|
||||||
|
|
||||||
|
|
||||||
class PreferenceNotToWork(BaseModel):
|
class PreferenceNotToWork(BaseModel):
|
||||||
date: datetime.date
|
date: datetime.date
|
||||||
reason: str = ""
|
reason: str = ""
|
||||||
|
|
||||||
|
|
||||||
class OutOfProgramme(BaseModel):
|
class OutOfProgramme(BaseModel):
|
||||||
start_date: datetime.date
|
start_date: datetime.date
|
||||||
end_date: datetime.date
|
end_date: datetime.date
|
||||||
reason: str = ""
|
reason: str = ""
|
||||||
|
|
||||||
|
|
||||||
class Worker(BaseModel):
|
class Worker(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
site: str
|
site: str
|
||||||
@@ -54,7 +76,7 @@ class Worker(BaseModel):
|
|||||||
not_available_to_work: list[NotAvailableToWork] = []
|
not_available_to_work: list[NotAvailableToWork] = []
|
||||||
pref_not_to_work: list[PreferenceNotToWork] = []
|
pref_not_to_work: list[PreferenceNotToWork] = []
|
||||||
work_requests: list[WorkRequests] = []
|
work_requests: list[WorkRequests] = []
|
||||||
remote_site: str = "plymouth", # We set a default proc_site
|
remote_site: str = ("plymouth",) # We set a default proc_site
|
||||||
previous_shifts: dict = {}
|
previous_shifts: dict = {}
|
||||||
shift_balance_extra: dict = {}
|
shift_balance_extra: dict = {}
|
||||||
bank_holiday_extra: int = 0
|
bank_holiday_extra: int = 0
|
||||||
@@ -96,6 +118,9 @@ class Worker(BaseModel):
|
|||||||
if self.start_date >= self.end_date:
|
if self.start_date >= self.end_date:
|
||||||
raise ValueError("End date must be after start date")
|
raise ValueError("End date must be after start date")
|
||||||
|
|
||||||
|
if self.name == "Steph bailey":
|
||||||
|
print(self.start_date, self.end_date)
|
||||||
|
|
||||||
# if no start date default to the start of the rota
|
# if no start date default to the start of the rota
|
||||||
if self.start_date is None:
|
if self.start_date is None:
|
||||||
self.calculated_start_date = Rota.start_date
|
self.calculated_start_date = Rota.start_date
|
||||||
@@ -144,9 +169,7 @@ class Worker(BaseModel):
|
|||||||
if isinstance(end_oop, datetime.date):
|
if isinstance(end_oop, datetime.date):
|
||||||
end_oop_date = end_oop
|
end_oop_date = end_oop
|
||||||
else:
|
else:
|
||||||
end_oop_date = datetime.datetime.strptime(
|
end_oop_date = datetime.datetime.strptime(end_oop, "%d/%m/%y").date()
|
||||||
end_oop, "%d/%m/%y"
|
|
||||||
).date()
|
|
||||||
|
|
||||||
if start_oop_date >= end_oop_date:
|
if start_oop_date >= end_oop_date:
|
||||||
raise ValueError("End OOP date must be after start date")
|
raise ValueError("End OOP date must be after start date")
|
||||||
@@ -178,10 +201,7 @@ class Worker(BaseModel):
|
|||||||
|
|
||||||
# loop throught dates converting to week / day combination
|
# loop throught dates converting to week / day combination
|
||||||
for item in self.pref_not_to_work:
|
for item in self.pref_not_to_work:
|
||||||
days_from_start = (
|
days_from_start = (item.date - Rota.start_date).days
|
||||||
item.date
|
|
||||||
- Rota.start_date
|
|
||||||
).days
|
|
||||||
week = days_from_start // 7 + 1
|
week = days_from_start // 7 + 1
|
||||||
day = Rota.days[(days_from_start % 7)]
|
day = Rota.days[(days_from_start % 7)]
|
||||||
# Weight the value to take into account the number of preferences
|
# Weight the value to take into account the number of preferences
|
||||||
@@ -195,22 +215,20 @@ class Worker(BaseModel):
|
|||||||
# print(not_available_to_work)
|
# print(not_available_to_work)
|
||||||
# loop throught dates converting to week / day combination
|
# loop throught dates converting to week / day combination
|
||||||
for item in self.not_available_to_work:
|
for item in self.not_available_to_work:
|
||||||
days_from_start = (
|
days_from_start = (item.date - Rota.start_date).days
|
||||||
item.date
|
|
||||||
- Rota.start_date
|
|
||||||
).days
|
|
||||||
week = days_from_start // 7 + 1
|
week = days_from_start // 7 + 1
|
||||||
day = Rota.days[(days_from_start % 7)]
|
day = Rota.days[(days_from_start % 7)]
|
||||||
Rota.unavailable_to_work.add((self.id, week, day))
|
Rota.unavailable_to_work.add((self.id, week, day))
|
||||||
Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason
|
Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason
|
||||||
|
|
||||||
for item in self.work_requests:
|
for item in self.work_requests:
|
||||||
days_from_start = (
|
days_from_start = (item.date - Rota.start_date).days
|
||||||
item.date
|
|
||||||
- Rota.start_date
|
|
||||||
).days
|
|
||||||
week = days_from_start // 7 + 1
|
week = days_from_start // 7 + 1
|
||||||
day = Rota.days[(days_from_start % 7)]
|
day = Rota.days[(days_from_start % 7)]
|
||||||
|
if item.shift == "*":
|
||||||
|
for shift in Rota.get_shifts_for_worker_site(self.site):
|
||||||
|
Rota.work_requests.add((self.id, week, day, shift.name))
|
||||||
|
else:
|
||||||
Rota.work_requests.add((self.id, week, day, item.shift))
|
Rota.work_requests.add((self.id, week, day, item.shift))
|
||||||
|
|
||||||
# Calculate the proportion of the rota that is being worked
|
# Calculate the proportion of the rota that is being worked
|
||||||
@@ -236,6 +254,9 @@ class Worker(BaseModel):
|
|||||||
|
|
||||||
self.non_working_day_list.append((item.day, start_date, end_date))
|
self.non_working_day_list.append((item.day, start_date, end_date))
|
||||||
|
|
||||||
|
if days_to_work < 1:
|
||||||
|
self.fte_adj = 0
|
||||||
|
|
||||||
def __lt__(self, other) -> bool:
|
def __lt__(self, other) -> bool:
|
||||||
return (self.site, self.grade, self.fte_adj, self.name) < (
|
return (self.site, self.grade, self.fte_adj, self.name) < (
|
||||||
other.site,
|
other.site,
|
||||||
@@ -246,7 +267,11 @@ class Worker(BaseModel):
|
|||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|
||||||
nwds = ", ".join([str(i) for i, start, end in self.non_working_day_list]) if self.non_working_day_list is not None else ""
|
nwds = (
|
||||||
|
", ".join([str(i) for i, start, end in self.non_working_day_list])
|
||||||
|
if self.non_working_day_list is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
return "{} {} [{}] REMOTE SITE:{}".format(
|
return "{} {} [{}] REMOTE SITE:{}".format(
|
||||||
self.name,
|
self.name,
|
||||||
(self.site, self.grade, self.fte_adj),
|
(self.site, self.grade, self.fte_adj),
|
||||||
|
|||||||
@@ -78,10 +78,7 @@ Rota.constraint_options["max_weekends"] = 7
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("exeter",),
|
sites=("exeter",), name="exeter_twilight", length= 12.5, days=days[:4],
|
||||||
"exeter_twilight",
|
|
||||||
12.5,
|
|
||||||
days[:4],
|
|
||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
constraints=["max_2_shifts_per_week"],
|
constraints=["max_2_shifts_per_week"],
|
||||||
),
|
),
|
||||||
@@ -89,37 +86,25 @@ Rota.add_shifts(
|
|||||||
("truro", "truro no proc"), "truro_twilight", 12.5, days[:4], balance_offset=3
|
("truro", "truro no proc"), "truro_twilight", 12.5, days[:4], balance_offset=3
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("torbay", "torbay twilights"),
|
sites=("torbay", "torbay twilights"), name="torbay_twilight", length= 12.5, days=days[:4],
|
||||||
"torbay_twilight",
|
|
||||||
12.5,
|
|
||||||
days[:4],
|
|
||||||
balance_offset=5,
|
balance_offset=5,
|
||||||
assign_as_block=True,
|
assign_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth", "plymouth_twilights"),
|
sites=("plymouth", "plymouth_twilights"), name="plymouth_twilight", length= 12.5, days=days[:5],
|
||||||
"plymouth_twilight",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
constraints=["max_2_shifts_per_week"],
|
constraints=["max_2_shifts_per_week"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("exeter",),
|
sites=("exeter",), name="weekend_exeter", length= 12.5, days=days[4:],
|
||||||
"weekend_exeter",
|
|
||||||
12.5,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraints=["preclear2", "postclear2"],
|
constraints=["preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("truro", "truro no proc"),
|
sites=("truro", "truro no proc"), name="weekend_truro", length= 12.5, days=days[4:],
|
||||||
"weekend_truro",
|
|
||||||
12.5,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
@@ -127,10 +112,7 @@ Rota.add_shifts(
|
|||||||
# force_as_block_unless_nwd=True
|
# force_as_block_unless_nwd=True
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("torbay",),
|
sites=("torbay",), name="weekend_torbay", length= 12.5, days=days[4:],
|
||||||
"weekend_torbay",
|
|
||||||
12.5,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
@@ -138,10 +120,7 @@ Rota.add_shifts(
|
|||||||
# force_as_block_unless_nwd=True
|
# force_as_block_unless_nwd=True
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth",),
|
sites=("plymouth",), name="weekend_plymouth1", length= 8, days=days[5:],
|
||||||
"weekend_plymouth1",
|
|
||||||
8,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
@@ -149,10 +128,7 @@ Rota.add_shifts(
|
|||||||
constraints=["preclear2", "postclear2"],
|
constraints=["preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth",),
|
sites=("plymouth",), name="weekend_plymouth2", length= 8, days=days[5:],
|
||||||
"weekend_plymouth2",
|
|
||||||
8,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
@@ -160,10 +136,7 @@ Rota.add_shifts(
|
|||||||
constraints=["preclear2", "postclear2"],
|
constraints=["preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("plymouth", "plymouth_twilights"),
|
sites=("plymouth", "plymouth_twilights"), name="plymouth_bank_holidays", length= 8, days=days[5:],
|
||||||
"plymouth_bank_holidays",
|
|
||||||
8,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=2,
|
balance_offset=2,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
@@ -171,10 +144,7 @@ Rota.add_shifts(
|
|||||||
bank_holidays_only=True,
|
bank_holidays_only=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
(sites[:-1]),
|
sites=(sites[:-1]), name="night_weekday", length= 12.25, days=days[:4],
|
||||||
"night_weekday",
|
|
||||||
12.25,
|
|
||||||
days[:4],
|
|
||||||
balance_offset=4.9,
|
balance_offset=4.9,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
# hard_constrain_shift=False,
|
# hard_constrain_shift=False,
|
||||||
@@ -184,10 +154,7 @@ Rota.add_shifts(
|
|||||||
constraints=["night", "preclear2", "postclear2"],
|
constraints=["night", "preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
(sites[:-1]),
|
sites=(sites[:-1]), name="night_weekend", length= 12.25, days=days[4:],
|
||||||
"night_weekend",
|
|
||||||
12.25,
|
|
||||||
days[4:],
|
|
||||||
balance_offset=3.9,
|
balance_offset=3.9,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
# hard_constrain_shift=False,
|
# hard_constrain_shift=False,
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import datetime
|
||||||
|
import pytest
|
||||||
|
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days
|
||||||
|
|
||||||
|
from rota.workers import Worker
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
def generate_basic_rota(weeks_to_rota=9):
|
||||||
|
start_date = datetime.date(2022, 3, 7)
|
||||||
|
|
||||||
|
Rota = RotaBuilder(
|
||||||
|
start_date,
|
||||||
|
weeks_to_rota=weeks_to_rota,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a few workers
|
||||||
|
Rota.add_workers(
|
||||||
|
[
|
||||||
|
Worker(name="worker1", site="group1", grade=1),
|
||||||
|
Worker(name="worker2", site="group1", grade=2),
|
||||||
|
Worker(name="worker3", site="group1", grade=3),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Rota
|
||||||
|
|
||||||
|
|
||||||
|
def date_generator(from_date, days):
|
||||||
|
n = 0
|
||||||
|
while True:
|
||||||
|
yield from_date
|
||||||
|
|
||||||
|
n = n + 1
|
||||||
|
if n >= days:
|
||||||
|
break
|
||||||
|
from_date = from_date + datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkerRequests:
|
||||||
|
def test_avoid_grade_constraint(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
|
workers_required=2,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([1], [1,2,3], ["a"])
|
||||||
|
Rota.add_grade_constraint_by_week([2], [4,5,6], ["a"])
|
||||||
|
Rota.add_grade_constraint_by_week([3], [7,8,9], ["a"])
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
Rota.export_rota_to_html("avoid_grade_constraint")
|
||||||
|
|
||||||
|
shift_summary = Rota.get_shift_summary_dict()
|
||||||
|
for worker_name in shift_summary:
|
||||||
|
worker_shifts = shift_summary[worker_name]
|
||||||
|
|
||||||
|
worker = Rota.get_worker_by_name(worker_name)
|
||||||
|
|
||||||
|
shift_string = Rota.get_worker_shift_list_string(worker)
|
||||||
|
|
||||||
|
match worker_name:
|
||||||
|
case "worker1":
|
||||||
|
assert shift_string.startswith("-"*21)
|
||||||
|
case "worker2":
|
||||||
|
assert shift_string.startswith(21*"a"+"-"*21)
|
||||||
|
case "worker3":
|
||||||
|
assert shift_string.endswith("-"*21)
|
||||||
|
|
||||||
|
|
||||||
|
def test_avoid_grade_constraint_invalid_shift(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
|
workers_required=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([1], [1,2,3], ["b"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1",), name="b", length= 12.5, days=days,
|
||||||
|
workers_required=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([1], [1,2,3], ["b"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_avoid_grade_constraint_invalid_grade(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
|
workers_required=2,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([4], [1,2,3], ["a"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
|
||||||
|
Rota.add_worker(
|
||||||
|
Worker(name="worker4", site="group1", grade=4),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([4], [1,2,3], ["a"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
|
def test_avoid_grade_constraint_multiple_grades(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
|
workers_required=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([1,2], [1,2,3], ["a"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([3], [1,2,3], ["a"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
assert Rota.results.solver.status == "warning"
|
||||||
|
|
||||||
|
def test_avoid_grade_constraint_multiple_shifts(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
|
workers_required=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1",), name="b", length= 12.5, days=days,
|
||||||
|
workers_required=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([1], [1,2,3], ["a", "b"])
|
||||||
|
Rota.add_grade_constraint_by_week([2], [4,5,6], ["a", "b"])
|
||||||
|
Rota.add_grade_constraint_by_week([3], [7,8,9], ["a", "b"])
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
Rota.export_rota_to_html("avoid_grade_constraint")
|
||||||
|
|
||||||
|
shift_summary = Rota.get_shift_summary_dict()
|
||||||
|
for worker_name in shift_summary:
|
||||||
|
worker_shifts = shift_summary[worker_name]
|
||||||
|
|
||||||
|
worker = Rota.get_worker_by_name(worker_name)
|
||||||
|
|
||||||
|
shift_string = Rota.get_worker_shift_list_string(worker)
|
||||||
|
|
||||||
|
match worker_name:
|
||||||
|
case "worker1":
|
||||||
|
assert shift_string.startswith("-"*21)
|
||||||
|
case "worker2":
|
||||||
|
assert "-"*21 in shift_string # hacky
|
||||||
|
case "worker3":
|
||||||
|
assert shift_string.endswith("-"*21)
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([2], [7,8,9], ["a"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
|
Rota.add_grade_constraint_by_week([1], [7,8,9], ["a"])
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
assert Rota.results.solver.status == "warning"
|
||||||
@@ -15,14 +15,16 @@ def generate_basic_rota(weeks_to_rota=10):
|
|||||||
Rota.constraint_options["balance_weekends"] = True
|
Rota.constraint_options["balance_weekends"] = True
|
||||||
|
|
||||||
# Add a few workers
|
# Add a few workers
|
||||||
Rota.add_workers([
|
Rota.add_workers(
|
||||||
|
[
|
||||||
Worker(name="worker1", site="group1", grade=1),
|
Worker(name="worker1", site="group1", grade=1),
|
||||||
Worker(name="worker2", site="group1", grade=1),
|
Worker(name="worker2", site="group1", grade=1),
|
||||||
Worker(name="worker3", site="group2", grade=1),
|
Worker(name="worker3", site="group2", grade=1),
|
||||||
# Worker(name="worker4", site="group2", grade=1),
|
# Worker(name="worker4", site="group2", grade=1),
|
||||||
# Worker(name="worker5", site="group2", grade=1),
|
# Worker(name="worker5", site="group2", grade=1),
|
||||||
# Worker(name="worker6", site="group2", grade=1, fte=50),
|
# Worker(name="worker6", site="group2", grade=1, fte=50),
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return Rota
|
return Rota
|
||||||
|
|
||||||
@@ -33,22 +35,22 @@ class TestBalancing:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=2,
|
balance_offset=2,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"b",
|
name="b",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=2,
|
balance_offset=2,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,30 +70,30 @@ class TestBalancing:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=["group1", "group2"],
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_weighting=4,
|
balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
constraint=["preclear2", "postclear2"],
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"b",
|
name="b",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"c",
|
name="c",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[0],
|
days=days[0],
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,31 +113,31 @@ class TestBalancing:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
# balance_weighting=4,
|
# balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
# constraint=["preclear2", "postclear2"],
|
# constraint=["preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"b",
|
name="b",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_weighting=4,
|
balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"c",
|
name="c",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[0],
|
days=days[0],
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -156,32 +158,32 @@ class TestBalancing:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
# balance_weighting=4,
|
# balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
# constraint=["preclear2", "postclear2"],
|
# constraint=["preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"b",
|
name="b",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_weighting=4,
|
balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"c",
|
name="c",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[0],
|
days=days[0],
|
||||||
balance_weighting=4,
|
balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -206,10 +208,10 @@ class TestBalancing:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=99,
|
balance_offset=99,
|
||||||
balance_weighting=8,
|
balance_weighting=8,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
@@ -217,14 +219,14 @@ class TestBalancing:
|
|||||||
# constraint=["preclear2", "postclear2"],
|
# constraint=["preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group2","group3"),
|
sites=("group2", "group3"),
|
||||||
"b",
|
name="b",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=99,
|
balance_offset=99,
|
||||||
# balance_weighting=4,
|
# balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -247,10 +249,10 @@ class TestBalancing:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=99,
|
balance_offset=99,
|
||||||
# balance_weighting=8,
|
# balance_weighting=8,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
@@ -258,14 +260,14 @@ class TestBalancing:
|
|||||||
# constraint=["preclear2", "postclear2"],
|
# constraint=["preclear2", "postclear2"],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group2","group3"),
|
sites=("group2", "group3"),
|
||||||
"b",
|
name="b",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=99,
|
balance_offset=99,
|
||||||
balance_weighting=4,
|
balance_weighting=4,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=False
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+11
-41
@@ -32,25 +32,19 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
# balance_offset=10,
|
# balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.10})
|
self.Rota.build_and_solve(options={"ratio": 0.10})
|
||||||
|
|
||||||
self.Rota.export_rota_to_html("nwd")
|
self.Rota.export_rota_to_html("nwd1")
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
assert self.Rota.results.solver.status == "ok"
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
assert self.Rota.results.solver.termination_condition == "optimal"
|
||||||
@@ -86,10 +80,7 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
@@ -131,10 +122,7 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
@@ -192,10 +180,7 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
@@ -234,10 +219,7 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block_unless_nwd=True,
|
force_as_block_unless_nwd=True,
|
||||||
@@ -290,20 +272,14 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block_unless_nwd=True,
|
force_as_block_unless_nwd=True,
|
||||||
assign_as_block=True,
|
assign_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block_unless_nwd=True,
|
force_as_block_unless_nwd=True,
|
||||||
@@ -359,20 +335,14 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block_unless_nwd=True,
|
force_as_block_unless_nwd=True,
|
||||||
assign_as_block=True,
|
assign_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block_unless_nwd=True,
|
force_as_block_unless_nwd=True,
|
||||||
|
|||||||
+8
-23
@@ -30,14 +30,11 @@ class TestRemoteRotas:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=100,
|
balance_offset=100,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
constraint=[("require_remote_site_presence_week", ("group2", 2))],
|
constraint=[{"name":"require_remote_site_presence_week", "options":("group2", 2)}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,20 +66,14 @@ class TestRemoteRotas:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days[:3],
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days[:3],
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("require_remote_site_presence_week", ("group2", 1))],
|
constraint=[{"name":"require_remote_site_presence_week", "options":("group2", 1)}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="b", length= 12.5, days=days[3:],
|
||||||
"b",
|
|
||||||
12.5,
|
|
||||||
days[3:],
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -135,20 +126,14 @@ class TestRemoteRotas:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days[:3],
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days[:3],
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
constraint=[("require_remote_site_presence", ("group2", 1))],
|
constraint=[{"name":"require_remote_site_presence_week", "options":("group2", 1)}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="b", length= 12.5, days=days[3:],
|
||||||
"b",
|
|
||||||
12.5,
|
|
||||||
days[3:],
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
|
|||||||
+311
-226
@@ -1,4 +1,5 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from re import S
|
||||||
from black import main
|
from black import main
|
||||||
import pytest
|
import pytest
|
||||||
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
|
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
|
||||||
@@ -17,7 +18,6 @@ class TestDemoRota:
|
|||||||
start_date,
|
start_date,
|
||||||
weeks_to_rota=weeks_to_rota,
|
weeks_to_rota=weeks_to_rota,
|
||||||
balance_offset_modifier=1,
|
balance_offset_modifier=1,
|
||||||
max_weekend_frequency=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Rota.constraint_options["max_shifts_per_week"] = 5
|
Rota.constraint_options["max_shifts_per_week"] = 5
|
||||||
@@ -44,10 +44,10 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"weekday",
|
name="weekday",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
# We use a different balance weighting for each shift as otherwise
|
# We use a different balance weighting for each shift as otherwise
|
||||||
@@ -55,10 +55,10 @@ class TestDemoRota:
|
|||||||
# balance_weighting=0.6,
|
# balance_weighting=0.6,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",),
|
||||||
"weekend",
|
name="weekend",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[5:],
|
days=days[5:],
|
||||||
balance_offset=4,
|
balance_offset=4,
|
||||||
# balance_weighting=0.5
|
# balance_weighting=0.5
|
||||||
),
|
),
|
||||||
@@ -191,7 +191,6 @@ class TestDemoRotaNights:
|
|||||||
start_date,
|
start_date,
|
||||||
weeks_to_rota=weeks_to_rota,
|
weeks_to_rota=weeks_to_rota,
|
||||||
balance_offset_modifier=1,
|
balance_offset_modifier=1,
|
||||||
max_weekend_frequency=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Rota.constraint_options["max_shifts_per_week"] = 5
|
Rota.constraint_options["max_shifts_per_week"] = 5
|
||||||
@@ -223,12 +222,12 @@ class TestDemoRotaNights:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"night_weekday",
|
name="night_weekday",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:4],
|
days=days[:4],
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
constraint=["preclear2", "postclear2"],
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
# constraint=["night"],
|
# constraint=["night"],
|
||||||
force_as_block=True
|
force_as_block=True
|
||||||
# We use a different balance weighting for each shift as otherwise
|
# We use a different balance weighting for each shift as otherwise
|
||||||
@@ -236,20 +235,20 @@ class TestDemoRotaNights:
|
|||||||
# balance_weighting=0.6,
|
# balance_weighting=0.6,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"night_weekend",
|
name="night_weekend",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[4:],
|
days=days[4:],
|
||||||
# balance_weighting=0.5
|
# balance_weighting=0.5
|
||||||
constraint=["preclear2", "postclear2"],
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
# constraint=["night"],
|
# constraint=["night"],
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"twilight",
|
name="twilight",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
# balance_weighting=0.5
|
# balance_weighting=0.5
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
# constraint=["preclear2", "postclear2"],
|
# constraint=["preclear2", "postclear2"],
|
||||||
@@ -271,11 +270,9 @@ class TestDemoRotaNights:
|
|||||||
|
|
||||||
Rota.export_rota_to_html("test3")
|
Rota.export_rota_to_html("test3")
|
||||||
|
|
||||||
|
|
||||||
def test_start_date(self):
|
def test_start_date(self):
|
||||||
assert self.Rota.start_date == self.start_date
|
assert self.Rota.start_date == self.start_date
|
||||||
|
|
||||||
|
|
||||||
def test_night_assignment(self):
|
def test_night_assignment(self):
|
||||||
shift_summary = self.Rota.get_shift_summary_dict()
|
shift_summary = self.Rota.get_shift_summary_dict()
|
||||||
|
|
||||||
@@ -303,7 +300,6 @@ class TestDemoRotaClear:
|
|||||||
start_date,
|
start_date,
|
||||||
weeks_to_rota=weeks_to_rota,
|
weeks_to_rota=weeks_to_rota,
|
||||||
balance_offset_modifier=1,
|
balance_offset_modifier=1,
|
||||||
max_weekend_frequency=1,
|
|
||||||
)
|
)
|
||||||
Rota.constraint_options["max_shifts_per_month"] = 20
|
Rota.constraint_options["max_shifts_per_month"] = 20
|
||||||
|
|
||||||
@@ -323,30 +319,30 @@ class TestDemoRotaClear:
|
|||||||
def test_preclear(self):
|
def test_preclear(self):
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
constraint=["preclear"],
|
constraint=[{"name": "preclear"}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"b",
|
name="b",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"c",
|
name="c",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[3],
|
days=days[3],
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
constraint=["preclear2", "postclear2"],
|
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -389,7 +385,6 @@ class TestDemoRotaShiftConstraints:
|
|||||||
start_date,
|
start_date,
|
||||||
weeks_to_rota=weeks_to_rota,
|
weeks_to_rota=weeks_to_rota,
|
||||||
balance_offset_modifier=1,
|
balance_offset_modifier=1,
|
||||||
max_weekend_frequency=1,
|
|
||||||
)
|
)
|
||||||
# Rota.constraint_options["max_shifts_per_week"] = 5
|
# Rota.constraint_options["max_shifts_per_week"] = 5
|
||||||
# Rota.constraint_options["max_shifts_per_month"] = 20
|
# Rota.constraint_options["max_shifts_per_month"] = 20
|
||||||
@@ -399,10 +394,10 @@ class TestDemoRotaShiftConstraints:
|
|||||||
Rota.add_workers((worker1, worker2))
|
Rota.add_workers((worker1, worker2))
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",),
|
||||||
"a",
|
name="a",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
@@ -479,14 +474,14 @@ class TestDemoRotaBalanceShiftSites:
|
|||||||
def test_balance_blocks_across_2_groups(self):
|
def test_balance_blocks_across_2_groups(self):
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:4],
|
days=days[:4],
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=["balance_across_groups"],
|
constraint=[{"name": "balance_across_groups"}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.Rota.constraint_options["balance_nights_across_sites"] = False
|
self.Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
@@ -515,14 +510,14 @@ class TestDemoRotaBalanceShiftSites:
|
|||||||
self.Rota.shifts = []
|
self.Rota.shifts = []
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:4],
|
days=days[:4],
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=["balance_across_groups"],
|
constraint=[{"name": "balance_across_groups"}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.Rota.constraint_options["balance_nights_across_sites"] = False
|
self.Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
@@ -549,14 +544,14 @@ class TestDemoRotaBalanceShiftSites:
|
|||||||
|
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:4],
|
days=days[:4],
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=["balance_across_groups"],
|
constraint=[{"name": "balance_across_groups"}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.Rota.constraint_options["balance_nights_across_sites"] = False
|
self.Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
@@ -580,8 +575,7 @@ class TestDemoRotaBalanceShiftSites:
|
|||||||
assert self.Rota.results.solver.status == "ok"
|
assert self.Rota.results.solver.status == "ok"
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
assert self.Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
|
def generate_limit_constraint_rota():
|
||||||
class TestLimitConstraints:
|
|
||||||
weeks_to_rota = 8
|
weeks_to_rota = 8
|
||||||
start_date = datetime.date(2022, 3, 7)
|
start_date = datetime.date(2022, 3, 7)
|
||||||
|
|
||||||
@@ -603,30 +597,41 @@ class TestLimitConstraints:
|
|||||||
|
|
||||||
Rota.shifts = []
|
Rota.shifts = []
|
||||||
|
|
||||||
|
return Rota
|
||||||
|
|
||||||
|
class TestLimitConstraints:
|
||||||
|
|
||||||
def test_constraint_limit_grades(self):
|
def test_constraint_limit_grades(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=60,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("limit_grade_number", {2: 1})],
|
constraint=[{"name": "limit_grade_number", "options": {2: 1}}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# self.Rota.constraint_options["balance_nights_across_sites"] = False
|
# Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
# self.Rota.constraint_options["balance_shifts_over_workers"] = False
|
# Rota.constraint_options["balance_shifts_over_workers"] = False
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
#Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.00})
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
|
Rota.export_rota_to_html("constraint_limit_grades")
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
|
grade_workers = Rota.get_workers_by_grade()
|
||||||
|
|
||||||
grade_workers = self.Rota.get_workers_by_grade()
|
|
||||||
for grade in grade_workers:
|
for grade in grade_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in grade_workers[grade]:
|
for w in grade_workers[grade]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
@@ -636,35 +641,33 @@ class TestLimitConstraints:
|
|||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("weekday_night") == limit
|
assert day_shifts.count("weekday_night") == limit
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
|
||||||
|
|
||||||
def test_constraint_limit_grades2(self):
|
def test_constraint_limit_grades2(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("limit_grade_number", {3: 2})],
|
constraint=[{"name": "limit_grade_number", "options": {3: 2}}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# self.Rota.constraint_options["balance_nights_across_sites"] = False
|
# Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
# self.Rota.constraint_options["balance_shifts_over_workers"] = False
|
# Rota.constraint_options["balance_shifts_over_workers"] = False
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
self.Rota.export_rota_to_html("test9")
|
Rota.export_rota_to_html("test9")
|
||||||
|
|
||||||
grade_workers = self.Rota.get_workers_by_grade()
|
grade_workers = Rota.get_workers_by_grade()
|
||||||
for grade in grade_workers:
|
for grade in grade_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in grade_workers[grade]:
|
for w in grade_workers[grade]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
@@ -674,35 +677,51 @@ class TestLimitConstraints:
|
|||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("weekday_night") == limit
|
assert day_shifts.count("weekday_night") == limit
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
assert Rota.results.solver.status == "ok"
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
def test_constraint_limit_grades3(self):
|
def test_constraint_limit_grades3(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
|
||||||
|
Rota.workers = []
|
||||||
|
|
||||||
|
worker1 = Worker(name="worker1", site="group1", grade=2, remote_site="group1")
|
||||||
|
worker2 = Worker(name="worker2", site="group1", grade=2, remote_site="group1")
|
||||||
|
worker3 = Worker(name="worker3", site="group1", grade=2, remote_site="group1")
|
||||||
|
worker4 = Worker(name="worker4", site="group3", grade=3, remote_site="group2")
|
||||||
|
worker5 = Worker(name="worker5", site="group3", grade=3, remote_site="group2")
|
||||||
|
worker6 = Worker(name="worker6", site="group3", grade=3, remote_site="group2")
|
||||||
|
worker7 = Worker(name="worker7", site="group3", grade=2, remote_site="group2")
|
||||||
|
|
||||||
|
Rota.add_workers((worker1, worker2, worker3, worker4))
|
||||||
|
Rota.add_workers((worker5, worker6))
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=60,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("limit_grade_number", {2: 3, 3: 1})],
|
constraint=[{"name": "limit_grade_number", "options": {2: 3, 3: 1}},]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# self.Rota.constraint_options["balance_nights_across_sites"] = False
|
# Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
# self.Rota.constraint_options["balance_shifts_over_workers"] = False
|
# Rota.constraint_options["balance_shifts_over_workers"] = False
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={"ratio": 0.01})
|
||||||
|
|
||||||
self.Rota.export_rota_to_html("test9")
|
Rota.export_rota_to_html("test9")
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
grade_workers = self.Rota.get_workers_by_grade()
|
grade_workers = Rota.get_workers_by_grade()
|
||||||
for grade in grade_workers:
|
for grade in grade_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in grade_workers[grade]:
|
for w in grade_workers[grade]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
@@ -712,55 +731,57 @@ class TestLimitConstraints:
|
|||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("weekday_night") == limit
|
assert day_shifts.count("weekday_night") == limit
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
|
||||||
|
|
||||||
def test_constraint_limit_grades4(self):
|
def test_constraint_limit_grades4(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("limit_grade_number", {2: 4, 3: 0})],
|
constraint=[{"name": "limit_grade_number", "options": {2: 4, 3: 0}}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# self.Rota.constraint_options["balance_nights_across_sites"] = False
|
# Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
# self.Rota.constraint_options["balance_shifts_over_workers"] = False
|
# Rota.constraint_options["balance_shifts_over_workers"] = False
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
assert self.Rota.results.solver.termination_condition == "infeasible"
|
assert Rota.results.solver.termination_condition == "infeasible"
|
||||||
|
|
||||||
def test_constraint_minimum_grades(self):
|
def test_constraint_minimum_grades(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("minimum_grade_number", (3, 1))],
|
constraint=[{"name": "minimum_grade_number", "options": (3, 1)}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
self.Rota.export_rota_to_html("test9")
|
Rota.export_rota_to_html("test9")
|
||||||
|
|
||||||
grade_workers = self.Rota.get_workers_by_grade()
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
|
grade_workers = Rota.get_workers_by_grade()
|
||||||
for grade in grade_workers:
|
for grade in grade_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in grade_workers[grade]:
|
for w in grade_workers[grade]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
@@ -770,33 +791,33 @@ class TestLimitConstraints:
|
|||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("weekday_night") == limit
|
assert day_shifts.count("weekday_night") == limit
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
|
||||||
|
|
||||||
def test_constraint_minimum_grades2(self):
|
def test_constraint_minimum_grades2(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("minimum_grade_number", (3, 3))],
|
constraint=[{"name": "minimum_grade_number", "options": (3, 3)}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
self.Rota.export_rota_to_html("test9")
|
Rota.export_rota_to_html("test9")
|
||||||
|
|
||||||
grade_workers = self.Rota.get_workers_by_grade()
|
grade_workers = Rota.get_workers_by_grade()
|
||||||
for grade in grade_workers:
|
for grade in grade_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in grade_workers[grade]:
|
for w in grade_workers[grade]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
@@ -806,71 +827,79 @@ class TestLimitConstraints:
|
|||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("weekday_night") == limit
|
assert day_shifts.count("weekday_night") == limit
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
def test_constraint_minimum_grades3(self):
|
def test_constraint_minimum_grades3(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("minimum_grade_number", (3, 0))],
|
constraint=[{"name": "minimum_grade_number", "options": (3, 0)}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
self.Rota.export_rota_to_html("test9")
|
Rota.export_rota_to_html("test9")
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
def test_constraint_minimum_grades_no_valid_worker(self):
|
def test_constraint_minimum_grades_no_valid_worker(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"weekday_night",
|
name="weekday_night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=4,
|
workers_required=4,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("minimum_grade_number", (4, 1))],
|
constraint=[{"name": "minimum_grade_number", "options": (4, 1)}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
def test_constraint_require_remote_site_presence_week(self):
|
def test_constraint_require_remote_site_presence_week(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"night",
|
name="night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=40,
|
balance_offset=40,
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("require_remote_site_presence_week", ("group1", 2))],
|
constraint=[
|
||||||
|
{
|
||||||
|
"name": "require_remote_site_presence_week",
|
||||||
|
"options": ("group1", 2),
|
||||||
|
}
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.00})
|
Rota.build_and_solve(options={"ratio": 0.00})
|
||||||
self.Rota.export_rota_to_html("remote1")
|
Rota.export_rota_to_html("remote1")
|
||||||
|
|
||||||
group_workers = self.Rota.get_workers_by_group()
|
group_workers = Rota.get_workers_by_group()
|
||||||
for group in group_workers:
|
for group in group_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in group_workers[group]:
|
for w in group_workers[group]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
@@ -880,67 +909,79 @@ class TestLimitConstraints:
|
|||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("night") == limit
|
assert day_shifts.count("night") == limit
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
assert Rota.results.solver.status == "ok"
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
def test_constraint_require_remote_site_presence_week2(self):
|
def test_constraint_require_remote_site_presence_week2(self):
|
||||||
self.Rota.constraint_options["minimise_shift_diffs"] = False
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = False
|
Rota.constraint_options["minimise_shift_diffs"] = False
|
||||||
self.Rota.constraint_options["balance_shift"] = False
|
Rota.constraint_options["balance_shifts_quadratic"] = False
|
||||||
self.Rota.constraint_options["balance_nights_across_sites"] = True
|
Rota.constraint_options["balance_shift"] = False
|
||||||
self.Rota.shifts = []
|
Rota.constraint_options["balance_nights_across_sites"] = True
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"night",
|
name="night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=20,
|
balance_offset=20,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("require_remote_site_presence_week", ("group2", 2))],
|
constraint=[
|
||||||
|
{
|
||||||
|
"name": "require_remote_site_presence_week",
|
||||||
|
"options": ("group1", 2),
|
||||||
|
}
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.00})
|
Rota.build_and_solve(options={"ratio": 0.00})
|
||||||
self.Rota.export_rota_to_html("remote2")
|
Rota.export_rota_to_html("remote2")
|
||||||
|
|
||||||
group_workers = self.Rota.get_workers_by_remote_group()
|
group_workers = Rota.get_workers_by_remote_group()
|
||||||
for group in group_workers:
|
for group in group_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in group_workers[group]:
|
for w in group_workers[group]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
if group == "group2":
|
if group == "group1":
|
||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("night") >= 2
|
assert day_shifts.count("night") >= 2
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
assert Rota.results.solver.status == "ok"
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
def test_constraint_require_remote_site_presence_week3(self):
|
def test_constraint_require_remote_site_presence_week3(self):
|
||||||
self.Rota.shifts = []
|
Rota = generate_limit_constraint_rota()
|
||||||
self.Rota.add_shifts(
|
Rota.shifts = []
|
||||||
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2", "group3"),
|
sites=("group1", "group2", "group3"),
|
||||||
"night",
|
name="night",
|
||||||
12.5,
|
length=12.5,
|
||||||
days,
|
days=days,
|
||||||
balance_offset=20,
|
balance_offset=20,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[("require_remote_site_presence_week", ("group2", 1))],
|
constraint=[
|
||||||
|
{
|
||||||
|
"name": "require_remote_site_presence_week",
|
||||||
|
"options": ("group2", 1),
|
||||||
|
}
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.00})
|
Rota.build_and_solve(options={"ratio": 0.00})
|
||||||
self.Rota.export_rota_to_html("remote3")
|
Rota.export_rota_to_html("remote3")
|
||||||
|
|
||||||
group_workers = self.Rota.get_workers_by_remote_group()
|
group_workers = Rota.get_workers_by_remote_group()
|
||||||
for group in group_workers:
|
for group in group_workers:
|
||||||
shift_patterns = []
|
shift_patterns = []
|
||||||
for w in group_workers[group]:
|
for w in group_workers[group]:
|
||||||
shift_patterns.append(self.Rota.get_worker_shift_list(w))
|
shift_patterns.append(Rota.get_worker_shift_list(w))
|
||||||
|
|
||||||
zipped_lists = list(zip(*shift_patterns))
|
zipped_lists = list(zip(*shift_patterns))
|
||||||
|
|
||||||
@@ -948,8 +989,9 @@ class TestLimitConstraints:
|
|||||||
for day_shifts in zipped_lists:
|
for day_shifts in zipped_lists:
|
||||||
assert day_shifts.count("night") >= 1
|
assert day_shifts.count("night") >= 1
|
||||||
|
|
||||||
assert self.Rota.results.solver.status == "ok"
|
assert Rota.results.solver.status == "ok"
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
|
|
||||||
# class TestNoWorkerRota:
|
# class TestNoWorkerRota:
|
||||||
# weeks_to_rota = 10
|
# weeks_to_rota = 10
|
||||||
@@ -979,23 +1021,41 @@ class TestNightUnavailable:
|
|||||||
|
|
||||||
# Add a few workers
|
# Add a few workers
|
||||||
worker1 = Worker(
|
worker1 = Worker(
|
||||||
name="worker1", site="group1", grade=1, not_available_to_work=[{ "date": datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(), "reason" : "****" },]
|
name="worker1",
|
||||||
|
site="group1",
|
||||||
|
grade=1,
|
||||||
|
not_available_to_work=[
|
||||||
|
{
|
||||||
|
"date": datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(),
|
||||||
|
"reason": "****",
|
||||||
|
},
|
||||||
|
],
|
||||||
)
|
)
|
||||||
worker2 = Worker(
|
worker2 = Worker(
|
||||||
name="worker2", site="group1", grade=1, not_available_to_work=[ { "date": datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(), "reason": "****" } ]
|
name="worker2",
|
||||||
|
site="group1",
|
||||||
|
grade=1,
|
||||||
|
not_available_to_work=[
|
||||||
|
{
|
||||||
|
"date": datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(),
|
||||||
|
"reason": "****",
|
||||||
|
}
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
Rota.add_workers((worker1, worker2))
|
Rota.add_workers((worker1, worker2))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_basic_assignment(self, capsys):
|
def test_basic_assignment(self, capsys):
|
||||||
self.Rota.shifts = []
|
self.Rota.shifts = []
|
||||||
|
|
||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",), "night_weekend", 12.5, days[5:], constraint=["night"]
|
sites =("group1",),
|
||||||
|
name = "night_weekend",
|
||||||
|
length = 12.5,
|
||||||
|
days = days[5:],
|
||||||
|
constraint=[{"name": "night"}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1005,7 +1065,6 @@ class TestNightUnavailable:
|
|||||||
assert self.Rota.results.solver.status == "ok"
|
assert self.Rota.results.solver.status == "ok"
|
||||||
assert self.Rota.results.solver.termination_condition == "optimal"
|
assert self.Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
|
|
||||||
self.Rota.export_rota_to_html("testnight")
|
self.Rota.export_rota_to_html("testnight")
|
||||||
summary = self.Rota.get_shift_summary_dict()
|
summary = self.Rota.get_shift_summary_dict()
|
||||||
|
|
||||||
@@ -1018,7 +1077,12 @@ class TestNightUnavailable:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",), "night_weekend", 12.5, days[5:], workers_required=2, constraint=["night"]
|
sites =("group1",),
|
||||||
|
name = "night_weekend",
|
||||||
|
length = 12.5,
|
||||||
|
days = days[5:],
|
||||||
|
workers_required=2,
|
||||||
|
constraint=[{"name": "night"}],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1032,7 +1096,12 @@ class TestNightUnavailable:
|
|||||||
|
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",), "night_weekend", 12.5, days[5:], workers_required=2, constraint=[]
|
sites = ("group1",),
|
||||||
|
name = "night_weekend",
|
||||||
|
length = 12.5,
|
||||||
|
days = days[5:],
|
||||||
|
workers_required=2,
|
||||||
|
constraint=[],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1044,16 +1113,31 @@ class TestNightUnavailable:
|
|||||||
def test_assign_split(self):
|
def test_assign_split(self):
|
||||||
self.Rota.shifts = []
|
self.Rota.shifts = []
|
||||||
worker3 = Worker(
|
worker3 = Worker(
|
||||||
name="worker3", site="group1", grade=1, not_available_to_work=({"date":datetime.datetime.strptime("13/03/22", "%d/%m/%y").date()},)
|
name="worker3",
|
||||||
|
site="group1",
|
||||||
|
grade=1,
|
||||||
|
not_available_to_work=(
|
||||||
|
{"date": datetime.datetime.strptime("13/03/22", "%d/%m/%y").date()},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
worker4 = Worker(
|
worker4 = Worker(
|
||||||
name="worker4", site="group1", grade=1, not_available_to_work=({"date":datetime.datetime.strptime("12/03/22", "%d/%m/%y").date()},)
|
name="worker4",
|
||||||
|
site="group1",
|
||||||
|
grade=1,
|
||||||
|
not_available_to_work=(
|
||||||
|
{"date": datetime.datetime.strptime("12/03/22", "%d/%m/%y").date()},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.Rota.add_workers((worker3, worker4))
|
self.Rota.add_workers((worker3, worker4))
|
||||||
|
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",), "night_weekend", 12.5, days[5:], workers_required=3, constraint=[]
|
sites=("group1",),
|
||||||
|
name="night_weekend",
|
||||||
|
length=12.5,
|
||||||
|
days=days[5:],
|
||||||
|
workers_required=3,
|
||||||
|
constraint=[],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1067,6 +1151,7 @@ class TestNightUnavailable:
|
|||||||
for worker in summary:
|
for worker in summary:
|
||||||
assert summary[worker]["night_weekend"] == 15
|
assert summary[worker]["night_weekend"] == 15
|
||||||
|
|
||||||
|
|
||||||
# def test_assign_split_night_constraint(self):
|
# def test_assign_split_night_constraint(self):
|
||||||
# self.Rota.shifts = []
|
# self.Rota.shifts = []
|
||||||
# worker3 = Worker(
|
# worker3 = Worker(
|
||||||
|
|||||||
+109
-8
@@ -49,10 +49,7 @@ class TestShifts:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,12 +60,116 @@ class TestShifts:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1"),
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(InvalidShift):
|
with pytest.raises(InvalidShift):
|
||||||
Rota.build_and_solve(options={"ratio": 0.000})
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
|
||||||
|
|
||||||
|
class TestShiftConstraints:
|
||||||
|
|
||||||
|
def test_max_shifts(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
|
constraint=[{"name": "max_shifts_per_week", "options": 4}],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
|
def test_max_shifts_fail(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
|
constraint=[{"name": "max_shifts_per_week", "options": 3}],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
|
||||||
|
assert Rota.results.solver.termination_condition == "infeasible"
|
||||||
|
|
||||||
|
Rota.add_worker(
|
||||||
|
Worker(
|
||||||
|
name="worker3", site="group1", grade=1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_shifts_per_week_block(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
|
constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 2}}],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
Rota.export_rota_to_html("max_shifts_per_week_block")
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_shifts_per_week_block_fail(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
|
constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 3}}],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
Rota.export_rota_to_html("max_shifts_per_week_block")
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "warning"
|
||||||
|
|
||||||
|
def test_max_shifts_per_week_block_shift_number(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
|
constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 2, "shift_number": 7}}],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
Rota.export_rota_to_html("max_shifts_per_week_block")
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
|
||||||
|
def test_max_shifts_per_week_block_shift_number_fail(self):
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
|
constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 2, "shift_number": 6}}],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
Rota.export_rota_to_html("max_shifts_per_week_block")
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "warning"
|
||||||
@@ -33,18 +33,12 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="b", length= 12.5, days=days[5:],
|
||||||
"b",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=0,
|
balance_offset=0,
|
||||||
workers_required=1
|
workers_required=1
|
||||||
),
|
),
|
||||||
@@ -97,19 +91,13 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
balance_offset=5,
|
balance_offset=5,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=True
|
force_as_block=True
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="b", length= 12.5, days=days[5:],
|
||||||
"b",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
balance_offset=0,
|
balance_offset=0,
|
||||||
workers_required=2
|
workers_required=2
|
||||||
),
|
),
|
||||||
@@ -159,18 +147,12 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
workers_required=1
|
workers_required=1
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -211,26 +193,17 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="c", length= 12.5, days=days[1],
|
||||||
"c",
|
|
||||||
12.5,
|
|
||||||
days[1],
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
workers_required=1
|
workers_required=1
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -259,26 +232,17 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="c", length= 12.5, days=days[1],
|
||||||
"c",
|
|
||||||
12.5,
|
|
||||||
days[1],
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
workers_required=1
|
workers_required=1
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -318,26 +282,17 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="c", length= 12.5, days=days[0],
|
||||||
"c",
|
|
||||||
12.5,
|
|
||||||
days[0],
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
workers_required=1
|
workers_required=1
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -368,26 +323,17 @@ class TestDemoRota:
|
|||||||
# Add a weekday and weekend shift
|
# Add a weekday and weekend shift
|
||||||
self.Rota.add_shifts(
|
self.Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="d", length= 12.5, days=days[:5],
|
||||||
"d",
|
|
||||||
12.5,
|
|
||||||
days[:5],
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="c", length= 12.5, days=days[0],
|
||||||
"c",
|
|
||||||
12.5,
|
|
||||||
days[0],
|
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",), name="w", length= 12.5, days=days[5:],
|
||||||
"w",
|
|
||||||
12.5,
|
|
||||||
days[5:],
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
force_as_block=True
|
force_as_block=True
|
||||||
),
|
),
|
||||||
|
|||||||
+112
-44
@@ -50,10 +50,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
@@ -90,10 +87,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
@@ -128,10 +122,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
@@ -173,10 +164,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
),
|
),
|
||||||
@@ -218,10 +206,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -255,10 +240,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -312,10 +294,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -356,10 +335,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=2,
|
workers_required=2,
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -401,10 +377,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
"a",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -417,10 +390,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="c", length= 12.5, days=days,
|
||||||
"c",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -432,10 +402,7 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"), name="b", length= 12.5, days=days,
|
||||||
"b",
|
|
||||||
12.5,
|
|
||||||
days,
|
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
balance_offset=1,
|
balance_offset=1,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -461,3 +428,104 @@ class TestWorkerRequests:
|
|||||||
|
|
||||||
with pytest.raises(InvalidShift):
|
with pytest.raises(InvalidShift):
|
||||||
Rota.build_and_solve(options={"ratio": 0.000})
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
|
||||||
|
def test_work_requests_generic(self):
|
||||||
|
"""_summary_"""
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
Rota.constraint_options["balance_bank_holidays"] = False
|
||||||
|
|
||||||
|
Rota.add_workers(
|
||||||
|
[Worker(
|
||||||
|
name="worker3", site="group1", grade=1,
|
||||||
|
work_requests=(
|
||||||
|
[
|
||||||
|
{"date": i, "shift": "*"}
|
||||||
|
for i in list(date_generator(datetime.date(2022, 3, 7), 35))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Worker(
|
||||||
|
name="worker4", site="group1", grade=1,
|
||||||
|
work_requests=(
|
||||||
|
[
|
||||||
|
{"date": i, "shift": "*"}
|
||||||
|
for i in list(date_generator(datetime.date(2022, 3, 7), 35))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),]
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days,
|
||||||
|
workers_required=2,
|
||||||
|
balance_offset=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.000})
|
||||||
|
Rota.export_rota_to_html("work_requests")
|
||||||
|
|
||||||
|
shift_summary = Rota.get_shift_summary_dict()
|
||||||
|
for worker_name in shift_summary:
|
||||||
|
worker_shifts = shift_summary[worker_name]
|
||||||
|
|
||||||
|
worker = Rota.get_worker_by_name(worker_name)
|
||||||
|
|
||||||
|
assert worker_shifts["a"] == 35
|
||||||
|
|
||||||
|
shift_string = Rota.get_worker_shift_list_string(worker)
|
||||||
|
|
||||||
|
if worker.name in ("worker3", "worker4"):
|
||||||
|
assert shift_string.startswith("a"*35)
|
||||||
|
|
||||||
|
def test_work_requests_generic2(self):
|
||||||
|
"""_summary_"""
|
||||||
|
Rota = generate_basic_rota()
|
||||||
|
Rota.constraint_options["balance_bank_holidays"] = False
|
||||||
|
|
||||||
|
Rota.add_workers(
|
||||||
|
[Worker(
|
||||||
|
name="worker3", site="group1", grade=1,
|
||||||
|
work_requests=(
|
||||||
|
[
|
||||||
|
{"date": i, "shift": "*"}
|
||||||
|
for i in list(date_generator(datetime.date(2022, 3, 7), 20))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.add_shifts(
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days[3:],
|
||||||
|
workers_required=2,
|
||||||
|
balance_offset=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
SingleShift(
|
||||||
|
sites=("group1", "group2"), name="b", length= 12.5, days=days[:3],
|
||||||
|
workers_required=1,
|
||||||
|
balance_offset=1,
|
||||||
|
force_as_block=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
Rota.export_rota_to_html("work_requests")
|
||||||
|
|
||||||
|
shift_summary = Rota.get_shift_summary_dict()
|
||||||
|
for worker_name in shift_summary:
|
||||||
|
worker_shifts = shift_summary[worker_name]
|
||||||
|
|
||||||
|
worker = Rota.get_worker_by_name(worker_name)
|
||||||
|
|
||||||
|
assert worker_shifts["a"] in (26, 27)
|
||||||
|
assert worker_shifts["b"] == 10
|
||||||
|
|
||||||
|
shift_string = Rota.get_worker_shift_list_string(worker)
|
||||||
|
|
||||||
|
if worker.name in ("worker3",):
|
||||||
|
assert "-" not in shift_string[:20]
|
||||||
+25
-18
@@ -22,14 +22,20 @@ class TestWorkers:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",),
|
||||||
"d",
|
name="d",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
SingleShift(("group1",), "w", 12.5, days[5:], workers_required=1),
|
SingleShift(
|
||||||
|
sites=("group1",),
|
||||||
|
name="w",
|
||||||
|
length=12.5,
|
||||||
|
days=days[5:],
|
||||||
|
workers_required=1,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(NoWorkers):
|
with pytest.raises(NoWorkers):
|
||||||
@@ -49,10 +55,10 @@ class TestWorkers:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1",),
|
sites=("group1",),
|
||||||
"d",
|
name="d",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
@@ -128,10 +134,10 @@ class TestWorkers:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"d",
|
name="d",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
@@ -206,7 +212,8 @@ class TestWorkers:
|
|||||||
worker3_oop = [
|
worker3_oop = [
|
||||||
{
|
{
|
||||||
"start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
|
"start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
|
||||||
"end_date": start_date + datetime.timedelta(weeks=3 * weeks_to_rota / 4),
|
"end_date": start_date
|
||||||
|
+ datetime.timedelta(weeks=3 * weeks_to_rota / 4),
|
||||||
"reason": "Quarter oop",
|
"reason": "Quarter oop",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -282,10 +289,10 @@ class TestWorkers:
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
("group1", "group2"),
|
sites=("group1", "group2"),
|
||||||
"d",
|
name="d",
|
||||||
12.5,
|
length=12.5,
|
||||||
days[:5],
|
days=days[:5],
|
||||||
balance_offset=10,
|
balance_offset=10,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
),
|
),
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI, Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from fastapi_sqlalchemy import DBSessionMiddleware, db
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from rota.shifts import SingleShift, ShiftConstraint
|
||||||
|
|
||||||
|
from web.models import SingleShift as ModelSingleShift
|
||||||
|
from web.models import ShiftConstraint as ModelShiftConstraint
|
||||||
|
|
||||||
|
#from rota.wokers
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv('../.env')
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.add_middleware(DBSessionMiddleware, db_url="sqlite:///test.db")
|
||||||
|
|
||||||
|
@app.post('/shifts/', response_model=SingleShift)
|
||||||
|
async def shifts(shift: ModelSingleShift):
|
||||||
|
db_shift = ModelSingleShift(name=shift.name, length=shift.length)
|
||||||
|
db.session.add(db_shift)
|
||||||
|
db.session.commit()
|
||||||
|
return db_shift
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/shifts/")
|
||||||
|
async def shifts():
|
||||||
|
shifts = db.session.query(ModelSingleShift).all()
|
||||||
|
|
||||||
|
return shifts
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "Hello World"}
|
||||||
|
|
||||||
|
@app.get("/files/{file_path:path}")
|
||||||
|
async def read_file(file_path: str):
|
||||||
|
return {"file_path": file_path}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Item(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str | None = None
|
||||||
|
price: float
|
||||||
|
tax: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/items/")
|
||||||
|
async def create_item(item: Item):
|
||||||
|
item.name + item.price
|
||||||
|
return item
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
uvicorn.run(app, host='0.0.0.0', port=8008)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Float, Boolean
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class SingleShift(Base):
|
||||||
|
__tablename__ = 'single_shift'
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String)
|
||||||
|
length = Column(Float)
|
||||||
|
balance_offset = Column(Float, nullable=True)
|
||||||
|
balance_weighting = Column(Float)
|
||||||
|
workers_required = Column(Float)
|
||||||
|
assign_as_block = Column(Boolean)
|
||||||
|
force_as_block = Column(Boolean)
|
||||||
|
hard_constrain_shift = Column(Boolean)
|
||||||
|
bank_holidays_only = Column(Boolean)
|
||||||
|
constraint_id = Column(Integer, ForeignKey('shift_constraint.id'))
|
||||||
|
|
||||||
|
constraint = relationship('ShiftConstraint')
|
||||||
|
|
||||||
|
|
||||||
|
class ShiftConstraint(Base):
|
||||||
|
__tablename__ = 'shift_constraint'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
options = Column(Integer, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import Body, FastAPI,Form, File, UploadFile
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/items/{item_id}")
|
||||||
|
async def read_items(
|
||||||
|
item_id: UUID,
|
||||||
|
start_datetime: datetime | None = Body(default=None),
|
||||||
|
end_datetime: datetime | None = Body(default=None),
|
||||||
|
repeat_at: time | None = Body(default=None),
|
||||||
|
process_after: timedelta | None = Body(default=None),
|
||||||
|
):
|
||||||
|
start_process = start_datetime + process_after
|
||||||
|
duration = end_datetime - start_process
|
||||||
|
return {
|
||||||
|
"item_id": item_id,
|
||||||
|
"start_datetime": start_datetime,
|
||||||
|
"end_datetime": end_datetime,
|
||||||
|
"repeat_at": repeat_at,
|
||||||
|
"process_after": process_after,
|
||||||
|
"start_process": start_process,
|
||||||
|
"duration": duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/files/")
|
||||||
|
async def create_files(files: list[bytes] = File()):
|
||||||
|
return {"file_sizes": [len(file) for file in files]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/uploadfiles/")
|
||||||
|
async def create_upload_files(files: list[UploadFile]):
|
||||||
|
return {"filenames": [file.filename for file in files]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def main():
|
||||||
|
content = """
|
||||||
|
<body>
|
||||||
|
<form action="/files/" enctype="multipart/form-data" method="post">
|
||||||
|
<input name="files" type="file" multiple>
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
||||||
|
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
|
||||||
|
<input name="files" type="file" multiple>
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
"""
|
||||||
|
return HTMLResponse(content=content)
|
||||||
Reference in New Issue
Block a user