Compare commits

...
11 Commits
Author SHA1 Message Date
Ross 242a664f49 ... 2025-01-25 08:49:14 +00:00
Ross 8459a4b3a9 start adding locum availability 2025-01-20 10:56:47 +00:00
Ross 7ce4677921 add worker requirements 2025-01-01 21:58:48 +00:00
Ross 9af01a3415 fix shift solver (objective didn't seem to be used) 2024-12-31 22:51:26 +00:00
Ross f0b9ee0ebc start working on shift paring balancing 2024-12-26 16:39:04 +00:00
Ross 1ed5f4fbcd work towards paring of shifts 2024-12-24 10:41:46 +00:00
Ross 70dc279a02 update from last round 2024-12-23 16:56:53 +00:00
Ross 51b4e89601 update requirements (removing fastapi) 2024-12-23 16:56:32 +00:00
Ross 44c21e3c6a update gen.py 2024-01-07 17:52:44 +00:00
Ross a7e1fa7171 numberous fixes 2024-01-07 17:52:36 +00:00
Ross 5d55ed77a8 update for current rota 2024-01-01 14:56:48 +00:00
21 changed files with 2542 additions and 818 deletions
+497 -318
View File
@@ -2,7 +2,11 @@ import datetime
import os
import sys
import time
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, WorkerRequirement, days
import typer
import subprocess
app = typer.Typer()
sites = (
@@ -15,6 +19,8 @@ sites = (
"proc",
)
NIGHT_REGISTRAR_NUMBER = 4
from rota.workers import (
Worker,
NotAvailableToWork,
@@ -25,344 +31,517 @@ from rota.workers import (
)
suspend_on_finish = False
solve = True
# time_to_run = 508500
# time_to_run = 2660000
time_to_run = 60 * 60 * 85
# allow = 5000
ratio = 0.001
# ratio = 0.9
@app.command()
def main(
suspend: bool = False,
solve: bool = True,
time_to_run: int = 60 * 60,
ratio: float = 0.001,
start_date: datetime.datetime = "2025-03-03",
weeks: int = 26,
bom: int = 1,
):
rota_start_date = start_date.date()
suspend_on_finish = suspend
Rota = RotaBuilder(
rota_start_date,
weeks_to_rota=weeks,
balance_offset_modifier=bom,
name="proc_rota",
)
# Rota = RotaBuilder(start_date, weeks_to_rota=20, balance_offset_modifier=1)
Rota.constraint_options["balance_shifts"] = False
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["max_night_frequency"] = 3 # 3
Rota.constraint_options["max_night_frequency_week_exclusions"] = []
Rota.constraint_options["max_weekend_frequency"] = 2
Rota.constraint_options["hard_constrain_pair_separation"] = True
# Rota.constraint_options["avoid_st2_first_month"] = True
start_date = datetime.date(2024, 3, 4)
# start_date = datetime.date(2023, 5, 1)
# start_date = start_date + datetime.timedelta(weeks=13)
# start_date = datetime.date(2022, 10, 10)
wr = [
WorkerRequirement(
end_date=datetime.datetime.strptime("2025-06-02", "%Y-%m-%d").date(),
number=3,
),
WorkerRequirement(
start_date=datetime.datetime.strptime("2025-06-02", "%Y-%m-%d").date(),
number=4,
),
]
Rota = RotaBuilder(
start_date, weeks_to_rota=26, balance_offset_modifier=1, name="proc rota"
)
# Rota = RotaBuilder(start_date, weeks_to_rota=20, balance_offset_modifier=1)
Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["max_night_frequency"] = 3 # 3
Rota.constraint_options["max_night_frequency_week_exclusions"] = []
Rota.constraint_options["max_weekend_frequency"] = 2
Rota.constraint_options["hard_constrain_pair_separation"] = True
# Rota.constraint_options["avoid_st2_first_month"] = True
Rota.add_shifts(
SingleShift(
sites=(
"exeter",
"exeter twilights and weekends",
"exeter no nights",
"exeter twilights",
),
name="exeter_twilight",
length=12.5,
days=days[:5],
balance_offset=4,
constraint=[
{
"name": "max_shifts_per_week",
"options": 2,
}
],
),
SingleShift(
sites=(
"truro",
"truro no nights",
"truro twilights",
"truro twilights and weekends",
"truro twilights and weekend nights",
"truro twilights, weekends and weekend nights",
),
name="truro_twilight",
length=12.5,
days=days[:4],
balance_offset=4,
),
SingleShift(
sites=("torbay", "torbay twilights", "torbay twilights and weekends"),
name="torbay_twilight",
length=12.5,
days=days[:4],
balance_offset=4,
assign_as_block=True,
),
SingleShift(
sites=(
"plymouth",
"plymouth twilights",
"plymouth twilights and weekends",
"plymouth twilights, weekends and weekend nights",
),
name="plymouth_twilight",
length=12.5,
days=days[:5],
balance_offset=4,
workers_required=1,
constraint=[{"name": "max_shifts_per_week", "options": 2}],
),
SingleShift(
sites=(
"exeter",
"exeter twilights and weekends",
"exeter no nights",
"exeter weekends",
),
name="weekend_exeter",
length=12.5,
days=days[5:],
balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 2}],
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
),
SingleShift(
sites=(
"truro",
"truro no nights",
"truro twilights and weekends",
"truro weekends",
"truro twilights, weekends and weekend nights",
),
name="weekend_truro",
length=12.5,
days=days[4:],
balance_offset=3,
# rota_on_nwds=True,
# force_as_block=True,
assign_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# force_as_block_unless_nwd=True
),
SingleShift(
sites=("torbay", "torbay twilights and weekends"),
name="weekend_torbay",
length=12.5,
days=days[4:],
balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# force_as_block_unless_nwd=True
),
SingleShift(
sites=(
"plymouth",
"plymouth twilights and weekends",
"plymouth first on weekends only",
"plymouth twilights, weekends and weekend nights",
),
name="weekend_plymouth1",
length=8,
days=days[5:],
balance_offset=2.9,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
),
SingleShift(
sites=(
"plymouth",
"plymouth twilights and weekends",
"plymouth twilights, weekends and weekend nights",
),
name="weekend_plymouth2",
length=8,
days=days[5:],
balance_offset=2.9,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
),
SingleShift(
sites=(
"plymouth",
"plymouth twilights",
"plymouth twilights and weekends",
"plymouth twilights, weekends and weekend nights",
),
name="plymouth_bank_holidays",
length=8,
days=days[5:],
balance_offset=1,
workers_required=1,
rota_on_nwds=True,
force_as_block=False,
bank_holidays_only=True,
),
SingleShift(
sites=sites,
name="night_weekday",
length=12.25,
days=days[:4],
balance_offset=3.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=wr,
force_as_block=True,
rota_on_nwds=True,
constraint=[
{"name": "night"},
{"name": "pre", "options": 2},
{"name": "post", "options": 2},
{
"name": "require_remote_site_presence_week",
"options": ("plymouth", 1),
},
{"name": "limit_grade_number", "options": {2: 1}},
{"name": "minimum_grade_number", "options": (4, 1)},
],
#end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
),
SingleShift(
sites=[
*sites,
"plymouth twilights, weekends and weekend nights",
"truro twilights and weekend nights",
"weekend nights",
"truro twilights, weekends and weekend nights",
],
name="night_weekend",
length=12.25,
days=days[4:],
balance_offset=2.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=wr,
force_as_block=True,
rota_on_nwds=True,
constraint=[
{"name": "night"},
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
{
"name": "require_remote_site_presence_week",
"options": ("plymouth", 1),
},
{"name": "limit_grade_number", "options": {2: 1}},
{"name": "minimum_grade_number", "options": (4, 1)},
],
#end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
),
# SingleShift(
# sites=sites,
# name="night_weekday4",
# length=12.25,
# days=days[:4],
# balance_offset=3.9,
# balance_weighting=1,
# # hard_constrain_shift=False,
# workers_required=NIGHT_REGISTRAR_NUMBER,
# force_as_block=True,
# rota_on_nwds=True,
# constraint=[
# {"name": "night"},
# {"name": "pre", "options": 2},
# {"name": "post", "options": 2},
# {
# "name": "require_remote_site_presence_week",
# "options": ("plymouth", 1),
# },
# {"name": "limit_grade_number", "options": {2: 1}},
# {"name": "minimum_grade_number", "options": (4, 1)},
# ],
# start_date=datetime.datetime.strptime("2025-06-02", "%Y-%m-%d").date(),
# ),
# SingleShift(
# sites=[
# *sites,
# "plymouth twilights, weekends and weekend nights",
# "truro twilights and weekend nights",
# "weekend nights",
# "truro twilights, weekends and weekend nights",
# ],
# name="night_weekend4",
# length=12.25,
# days=days[4:],
# balance_offset=2.9,
# balance_weighting=1,
# # hard_constrain_shift=False,
# workers_required=NIGHT_REGISTRAR_NUMBER,
# force_as_block=True,
# rota_on_nwds=True,
# constraint=[
# {"name": "night"},
# {"name": "pre", "options": 2},
# {"name": "post", "options": 3},
# {
# "name": "require_remote_site_presence_week",
# "options": ("plymouth", 1),
# },
# {"name": "limit_grade_number", "options": {2: 1}},
# {"name": "minimum_grade_number", "options": (4, 1)},
# ],
# start_date=datetime.datetime.strptime("2025-06-02", "%Y-%m-%d").date(),
# ),
)
Rota.add_shifts(
SingleShift(
sites=("exeter", "exeter twilights and weekends", "exeter no nights"),
name="exeter_twilight",
length=12.5,
days=days[:5],
balance_offset=4,
constraint=[
{
"name": "max_shifts_per_week",
"options": 2,
}
],
),
SingleShift(
sites=("truro", "truro twilights"),
name="truro_twilight",
length=12.5,
days=days[:4],
balance_offset=4,
),
SingleShift(
sites=("torbay", "torbay twilights"),
name="torbay_twilight",
length=12.5,
days=days[:4],
balance_offset=4,
assign_as_block=True,
),
SingleShift(
sites=("plymouth", "plymouth twilights", "plymouth twilights and weekends"),
name="plymouth_twilight",
length=12.5,
days=days[:5],
balance_offset=4,
workers_required=1,
constraint=[{"name": "max_shifts_per_week", "options": 2}],
),
SingleShift(
sites=("exeter", "exeter twilights and weekends", "exeter no nights"),
name="weekend_exeter",
length=12.5,
days=days[5:],
balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 2}],
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
),
SingleShift(
sites=("truro",),
name="weekend_truro",
length=12.5,
days=days[4:],
balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# force_as_block_unless_nwd=True
),
SingleShift(
sites=("torbay",),
name="weekend_torbay",
length=12.5,
days=days[4:],
balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# force_as_block_unless_nwd=True
),
SingleShift(
sites=("plymouth", "plymouth twilights and weekends", "plymouth first on weekends only"),
name="weekend_plymouth1",
length=8,
days=days[5:],
balance_offset=2.9,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
),
SingleShift(
sites=("plymouth", "plymouth twilights and weekends"),
name="weekend_plymouth2",
length=8,
days=days[5:],
balance_offset=2.9,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
),
SingleShift(
sites=("plymouth", "plymouth twilights", "plymouth twilights and weekends"),
name="plymouth_bank_holidays",
length=8,
days=days[5:],
balance_offset=1,
workers_required=1,
rota_on_nwds=True,
force_as_block=False,
bank_holidays_only=True,
),
SingleShift(
sites=sites,
name="night_weekday",
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": "pre", "options": 2},
{"name": "post", "options": 2},
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
{"name": "limit_grade_number", "options": {2: 1}},
{"name": "minimum_grade_number", "options": (4, 1)},
],
),
SingleShift(
sites=(sites),
name="night_weekend",
length=12.25,
days=days[4:],
balance_offset=2.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=3,
force_as_block=True,
rota_on_nwds=True,
constraint=[
{"name": "night"},
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
{"name": "limit_grade_number", "options": {2: 1}},
{"name": "minimum_grade_number", "options": (4, 1)},
],
),
)
# Rota.add_grade_constraint_by_week([2], [1, 2], ["night_weekday", "night_weekend"])
# Rota.add_grade_constraint_by_week([2], [1, 2], ["night_weekday", "night_weekend"])
load_leave = True
Rota.build_shifts()
# Rota.add_grade_constraint_by_week([2], [1], Rota.get_shift_names())
load_leave = True
Rota.build_shifts()
# Rota.add_grade_constraint_by_week([2], [1], Rota.get_shift_names())
if load_leave:
import leave
if load_leave:
import leave
workers = leave.load_leave(Rota)
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"]
try:
fte = float(w["fte"]) * 100
except ValueError:
print(f"{worker} has invalid fte: {fte}")
raise ValueError
n = 0
for worker in workers:
n = n + 1
worker_name = worker
w = workers[worker]
site = w["site"]
grade = w["grade"]
try:
fte = float(w["fte"]) * 100
except ValueError:
print(f"{worker} has invalid fte: {fte}")
raise ValueError
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
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
# 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
nwds = []
if nwd:
for i in nwd.split(","):
nwd_start_date = Rota.start_date
nwd_end_date = Rota.rota_end_date
if "[" in i:
print("Split nwd", worker_name, i)
a, b = i.split("[")[1][:-1].split("-")
print(f"{a=} {b=}")
nwd_start_date = datetime.datetime.strptime(
a, "%d/%m/%Y"
).date()
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%Y").date()
if "[" in i:
print("Split nwd", worker_name, i)
a, b = i.split("[")[1][:-1].split("-")
print(f"{a=} {b=}")
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.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
# nwds = [i.strip()[:3].capitalize() for i in nwd.split("/")] if nwd else None
end_date = end_date if end_date else None
if oop:
formatted_oops = []
for dates in oop.split(","):
s, e = dates.split("-")
formatted_oops.append(
{
"start_date": datetime.datetime.strptime(
s.strip(), "%d/%m/%y"
).date(),
"end_date": datetime.datetime.strptime(
e.strip(), "%d/%m/%y"
).date(),
}
)
if oop:
formatted_oops = []
for dates in oop.split(","):
print(dates)
if "-" in dates:
s, e = dates.split("-")
elif "to" in dates:
s, e = dates.split(" to ")
else:
raise ValueError(f"Cannot parse OOP dates: '{oop}' for {worker}")
try:
formatted_oops.append(
{
"start_date": datetime.datetime.strptime(
s.strip(), "%d/%m/%Y"
).date(),
"end_date": datetime.datetime.strptime(
e.strip(), "%d/%m/%Y"
).date(),
}
)
except ValueError:
try:
formatted_oops.append(
{
"start_date": datetime.datetime.strptime(
s.strip(), "%d/%m/%y"
).date(),
"end_date": datetime.datetime.strptime(
e.strip(), "%d/%m/%y"
).date(),
}
)
except ValueError as e:
print(e)
print("WORKER", worker)
print("DATES", s, e)
raise
oop = formatted_oops
else:
oop = []
oop = formatted_oops
else:
oop = []
print(f"{worker} OOP {oop}")
print(f"{worker} OOP {oop}")
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)
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"]
leave_requests = w["leave"]
work_requests = w["requests"]
site_pref = w["site_pref"]
w = 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,
oop=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"],
)
shift_fte_overrides = {}
print(w)
if worker_name == "Ben Kemp":
shift_fte_overrides = {
"plymouth_twilight": 100,
"weekend_plymouth1": 50,
"weekend_plymouth2": 50,
}
elif worker_name == "Joel Lim":
shift_fte_overrides = {
"plymouth_twilight": 100,
"weekend_exeter": 50,
}
Rota.add_worker(w)
w = 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,
oop=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"],
shift_fte_overrides=shift_fte_overrides,
)
print(w)
Rota.add_worker(w)
# Rota.build_workers()
# Rota.build_model()
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
# solver_options = {"seconds": time_to_run, "threads": 10}
# start_time = time.time()
Rota.build_and_solve(solver_options, export=True, solve=solve, solver="appsi_highs")
# Rota.solve_shifts_by_block(solver_options, block_length=13)
# Rota.solve_shifts_individually(solver_options)
# Rota.solve_model(options=solver_options)
# end_time = time.time()
# print(f"Time taken {end_time-start_time}")
# 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")
subprocess.run(
["scp", Rota.exported_rota_file, "ross@46.101.13.46:proc/proc-rota/output/"]
)
if suspend_on_finish:
os.system("systemctl suspend")
# Rota.build_workers()
# Rota.build_model()
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
#start_time = time.time()
Rota.build_and_solve(solver_options, export=True)
# Rota.solve_shifts_by_block(solver_options, block_length=13)
# Rota.solve_shifts_individually(solver_options)
# Rota.solve_model(options=solver_options)
#end_time = time.time()
#print(f"Time taken {end_time-start_time}")
# 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")
if __name__ == "__main__":
app()
+45 -15
View File
@@ -2,6 +2,9 @@ import csv
from datetime import datetime
import re
from requests import Session
from loguru import logger
from rich.pretty import pprint
from rota.workers import Worker, NotAvailableToWork, NonWorkingDays, WorkRequests, PreferenceNotToWork, OutOfProgramme
@@ -9,6 +12,8 @@ from rota.workers import Worker, NotAvailableToWork, NonWorkingDays, WorkRequest
date_re = r"[\d]{1,2}\/[\d]{1,2}\/[\d]{2}"
live_rota = True
def load_leave(Rota):
with Session() as s:
@@ -21,11 +26,17 @@ def load_leave(Rota):
"Pragma": "no-cache"
}
download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vTQBxt0mHoVS1D5EkQcrekqGveWyTG2MCMyV1NnrSWkptqWbdI6_myaeLT1vpbkbujXJqU6Z5nrzTh9/pub?gid=0&single=true&output=csv", headers=headers)
decoded_content = download.content.decode('utf-8')
if live_rota:
download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vSRx9VWXSlRubPyA0RhiI-Oqf5eHNYYEc6rFzlraDbR5_8qqr5g13-4uV-gn4u-TjZxiSMv1fBUaESq/pub?gid=814517272&single=true&output=csv", headers=headers)
decoded_content = download.content.decode('utf-8')
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
else:
with open("/home/ross/Downloads/PROC Draft RK version.csv") as f:
reader = csv.reader(f.read().splitlines(), delimiter=',')
n = 0
leave = {}
requests = {}
site_prefs = {}
@@ -34,7 +45,11 @@ def load_leave(Rota):
n = 1
for row in reader:
row_title = row[0]
if n < 10:
print(row)
row_title = row[1]
print(row_title)
row_date = row[0]
r = row[2:]
# header, extract names
if n == 1:
@@ -48,6 +63,9 @@ def load_leave(Rota):
workers[name]["site_pref"] = []
workers[name]["shift_balance_extra"] = {}
workers[name]["bank_holiday_extra"] = 0
workers[name]["end_date"] = None
workers[name]["start_date"] = None
workers[name]["pair"] = None
for i in range(len(r)):
try:
@@ -64,19 +82,19 @@ def load_leave(Rota):
if lower_item == "derriford twilights":
lower_item = "plymouth_twilights"
if row_title == "PROC site":
if "PROC site" in row_title or row_title == "PROC nights from":
worker["site_pref"] = lower_item
if row_title == "Rotation":
elif row_title in ("Rotation", "Site", "Placement", "Placement location"):
worker["site"] = lower_item
elif row_title == "Grade":
elif row_title in ("Grade", "Grade (ST)"):
worker["grade"] = lower_item
elif row_title == "%FTE":
elif row_title in ("%FTE", "%FTE on-call", "FTE", "FTE on-call"):
worker["fte"] = lower_item
elif row_title == "NWD":
elif "NWD" in row_title:
worker["nwd"] = lower_item
elif row_title == "Flexible NWD":
@@ -92,12 +110,18 @@ def load_leave(Rota):
elif row_title in ("Start date",):
if lower_item:
date = datetime.strptime(lower_item, "%d/%m/%y").date()
try:
date = datetime.strptime(lower_item, "%d/%m/%y").date()
except ValueError:
date = datetime.strptime(lower_item, "%d/%m/%Y").date()
except ValueError:
logger.warning(f"Cannot parse date: {lower_item} (n: {n}, row: {row})")
raise ValueError(f"Cannot parse date: {lower_item}")
worker["start_date"] = date
else:
worker["start_date"] = None
elif row_title == "OOP":
elif "OOP" in row_title:
worker["oop"] = lower_item
elif row_title == "Group":
@@ -122,19 +146,25 @@ def load_leave(Rota):
if 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_date) is not None:
if lower_item != "":
try:
try:
date = datetime.strptime(row[0], "%d/%m/%y").date()
date = datetime.strptime(row_date, "%d/%m/%y").date()
except ValueError:
date = datetime.strptime(row[0], "%d/%m/%Y").date()
date = datetime.strptime(row_date, "%d/%m/%Y").date()
# This may be easier to do as a dict
if lower_item in shifts:
worker["requests"].append(WorkRequests(date=date, shift=lower_item))
elif lower_item in ("nights only"):
if date.weekday() < 5:
worker["requests"].append(WorkRequests(date=date, shift="night_weekday"))
else:
worker["requests"].append(WorkRequests(date=date, shift="night_weekend"))
elif lower_item in ("work_request", "happy to work", "offered"):
elif lower_item in ("work_request", "happy to work", "offered", "may be", "volunteered"):
worker["requests"].append(WorkRequests(date=date, shift="*"))
-95
View File
@@ -1,95 +0,0 @@
"Name","Madalina Drumea","Layla Bell","Alex wood","Max Ireland","Seren Peters","Charles Finan","Paul Ward","Delilah Trimmer","Tom Welsh","Kyaw Tint","Maththew O'Brien","Max Finzel","Zainab Sharaf","Jean Skumar","Michael Tomek","Dhuvresh Patel","Nicolas Dziadulewicz","Vilim Kalamar","Georgina Edwards","Jenn Haw fong","Paul Jenkins","Amoolya Mannava","Rebecca Murphy","Ross Kruger","Fiona Lyall","Wijhdan Abusrewil","Andrew MacCormick","Joel Lim","Mark Haley","Alexander Sanchez - cabello","Chong Yew Ng","Matthew Thorley","Sayed Hashim Alqarooni","Harriet Conley","Jon Skinner","Alexander Wijnberg","Cameron Bullock","Alan Eccles","Christian Greer","Hannah Lewis","Amina Odeh","Sarath Vennam","Mo Babsail","Richard Chaytor","Sherafghan PROC"
"Site","barnstaple","exeter","exeter","exeter","exeter","exeter","exeter","exeter","exeter","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth","plymouth_twilights","torbay","torbay","torbay","torbay","torbay","torbay","torbay","torbay","torbay","torbay twilights","truro","truro","truro","truro","truro","truro","truro","truro","truro","truro no proc"
"Grade","3","2","3","3","3","4","4","5","5","2","2","2","2","3","3","3","3","3","4","4","4","5","5","5","6","2","2","2","2","3","4","4","4","5","6","2","2","3","3","3","4","4","5","5","5"
"FTE","80","100","100","100","100","100","100","80","100","100","100","100","100","60","80","100","100","100","80","100","100","100","100","100","60","100","100","100","100","100","100","100","100","60","100","100","100","80","100","100","100","100","100","100","100"
"Week 1 Day Mon","","","","","","exeter_twilight","","","","","night_weekday","","","","","","night_weekday","","","","","plymouth_twilight","","","","","","","torbay_twilight","","","","","","","","","","","","","night_weekday","","","truro_twilight"
"Week 1 Day Tue","","","","","","","","exeter_twilight","","","night_weekday","","plymouth_twilight","","","","night_weekday","","","","","","","","","","","","","","","","","torbay_twilight","","","","","","","","night_weekday","","","truro_twilight"
"Week 1 Day Wed","","","","","","exeter_twilight","","","","","night_weekday","","plymouth_twilight","","","","night_weekday","","","","","","","","","","","","torbay_twilight","","","","","","","","","","","","","night_weekday","truro_twilight","",""
"Week 1 Day Thu","","","","","exeter_twilight","","","","","","night_weekday","","","","","","night_weekday","","","","plymouth_twilight","","","","","","","torbay_twilight","","","","","","","","","","","","","","night_weekday","truro_twilight","",""
"Week 1 Day Fri","","","night_weekend","weekend_exeter","","","","","","","","","","plymouth_twilight","","night_weekend","","","","","","","","","","","","","","","","","","weekend_torbay","","","","night_weekend","","","weekend_truro","","","",""
"Week 1 Day Sat","","","night_weekend","weekend_exeter","","","","","","","","","weekend_plymouth1","","weekend_plymouth2","night_weekend","","","","","","","","","","","","","","","","","","weekend_torbay","","","","night_weekend","","","weekend_truro","","","",""
"Week 1 Day Sun","","","night_weekend","weekend_exeter","","","","","","","","","weekend_plymouth1","","weekend_plymouth2","night_weekend","","","","","","","","","","","","","","","","","","weekend_torbay","","","","night_weekend","","","weekend_truro","","","",""
"Week 2 Day Mon","","","","","","","exeter_twilight","","","","","","","","","","","plymouth_twilight","night_weekday","","","","","","","","night_weekday","night_weekday","torbay_twilight","","","","","","","","","","","","","truro_twilight","","",""
"Week 2 Day Tue","","","","","","","exeter_twilight","","","","","","","","","","plymouth_twilight","","night_weekday","","","","","","","","night_weekday","night_weekday","torbay_twilight","","","","","","","","","","","","","","truro_twilight","",""
"Week 2 Day Wed","","","exeter_twilight","","","","","","","","","","","","","","","","night_weekday","","plymouth_twilight","","","","","","night_weekday","night_weekday","torbay_twilight","","","","","","","","","","","","","","","truro_twilight",""
"Week 2 Day Thu","","","exeter_twilight","","","","","","","","","","","","","","","","night_weekday","","","","","","plymouth_twilight","","night_weekday","night_weekday","torbay_twilight","","","","","","","","truro_twilight","","","","","","","",""
"Week 2 Day Fri","","","","","","night_weekend","weekend_exeter","","","","","","","","","plymouth_twilight","","night_weekend","","","","","","","","","","","","","weekend_torbay","","night_weekend","","","","","","","","","weekend_truro","","",""
"Week 2 Day Sat","","","","","","night_weekend","weekend_exeter","","","","weekend_plymouth2","","","","","","","night_weekend","","","","","weekend_plymouth1","","","","","","","","weekend_torbay","","night_weekend","","","","","","","","","weekend_truro","","",""
"Week 2 Day Sun","","","","","","night_weekend","weekend_exeter","","","","weekend_plymouth2","","","","","","","night_weekend","","","","","weekend_plymouth1","","","","","","","","weekend_torbay","","night_weekend","","","","","","","","","weekend_truro","","",""
"Week 3 Day Mon","","","exeter_twilight","","","","","","","night_weekday","","","","","plymouth_twilight","night_weekday","","","","","","","","","","","","torbay_twilight","","","","","","","","night_weekday","","","","truro_twilight","","","","",""
"Week 3 Day Tue","","","exeter_twilight","","","","","","","night_weekday","","plymouth_twilight","","","","night_weekday","","","","","","","","","","","","torbay_twilight","","","","","","","","night_weekday","","","truro_twilight","","","","","",""
"Week 3 Day Wed","","","","","","exeter_twilight","","","","night_weekday","","","plymouth_twilight","","","night_weekday","","","","","","","","","","","","torbay_twilight","","","","","","","","night_weekday","","","","","","","truro_twilight","",""
"Week 3 Day Thu","","","","","exeter_twilight","","","","","night_weekday","","","","","","night_weekday","","","plymouth_twilight","","","","","","","","","torbay_twilight","","","","","","","","night_weekday","","","","","","","truro_twilight","",""
"Week 3 Day Fri","","weekend_exeter","","","","","","","","","","","","","","","","plymouth_twilight","","night_weekend","","night_weekend","","","","","","","night_weekend","weekend_torbay","","","","","","","","","","","","","","","weekend_truro"
"Week 3 Day Sat","","weekend_exeter","","","","","","","","","","","","weekend_plymouth2","weekend_plymouth1","","","","","night_weekend","","night_weekend","","","","","","","night_weekend","weekend_torbay","","","","","","","","","","","","","","","weekend_truro"
"Week 3 Day Sun","","weekend_exeter","","","","","","","","","","","","weekend_plymouth2","weekend_plymouth1","","","","","night_weekend","","night_weekend","","","","","","","night_weekend","weekend_torbay","","","","","","","","","","","","","","","weekend_truro"
"Week 4 Day Mon","","","","","","exeter_twilight","","night_weekday","","","","","","","","","","","","","","","plymouth_twilight","","","","","","","","","","night_weekday","","torbay_twilight","truro_twilight","","","","night_weekday","","","","",""
"Week 4 Day Tue","","","exeter_twilight","","","","","night_weekday","","","","","","","","plymouth_twilight","","","","","","","","","","","","","","","","torbay_twilight","night_weekday","","","","","","","night_weekday","","truro_twilight","","",""
"Week 4 Day Wed","","","","exeter_twilight","","","","night_weekday","","","","","","","","plymouth_twilight","","","","","","","","","","","","","torbay_twilight","","","","night_weekday","","","","","","","night_weekday","","","","","truro_twilight"
"Week 4 Day Thu","","","","","","","","night_weekday","exeter_twilight","plymouth_twilight","","","","","","","","","","","","","","","","","","","","","","","night_weekday","","torbay_twilight","","","","","night_weekday","","","","","truro_twilight"
"Week 4 Day Fri","","","","","weekend_exeter","","","","","","","","plymouth_twilight","","","","","","","","","","","night_weekend","","","weekend_torbay","","","","","","","","","weekend_truro","","","night_weekend","","","night_weekend","","",""
"Week 4 Day Sat","","","","","weekend_exeter","","","","","","weekend_plymouth1","","","","","","","","","","","","weekend_plymouth2","night_weekend","","","weekend_torbay","","","","","","","","","weekend_truro","","","night_weekend","","","night_weekend","","",""
"Week 4 Day Sun","","","","","weekend_exeter","","","","","","weekend_plymouth1","","","","","","","","","","","","weekend_plymouth2","night_weekend","","","weekend_torbay","","","","","","","","","weekend_truro","","","night_weekend","","","night_weekend","","",""
"Week 5 Day Mon","","exeter_twilight","","","","","night_weekday","","","plymouth_twilight","","","","","","","","","","","","","","","","","","","night_weekday","","","","torbay_twilight","night_weekday","","","","truro_twilight","","","","","","",""
"Week 5 Day Tue","","","","","","","night_weekday","","exeter_twilight","","","","","","","","","","","","plymouth_twilight","","","","","","","","night_weekday","","","","torbay_twilight","night_weekday","","","","","","","","","truro_twilight","",""
"Week 5 Day Wed","","","","","","","night_weekday","","exeter_twilight","","","","","","","","","","","","plymouth_twilight","","","","","","","","night_weekday","","","","torbay_twilight","night_weekday","","","","","","","","","","truro_twilight",""
"Week 5 Day Thu","","","","","exeter_twilight","","night_weekday","","","","","","","","","","","","","","","plymouth_twilight","","","","","","","night_weekday","","","","torbay_twilight","night_weekday","","","","","truro_twilight","","","","","",""
"Week 5 Day Fri","","","","night_weekend","","","","weekend_exeter","","","","","","","","","","","","","","","","plymouth_twilight","","","","night_weekend","","weekend_torbay","night_weekend","","","","","","","","","","weekend_truro","","","",""
"Week 5 Day Sat","","","","night_weekend","","","","weekend_exeter","","","","","","","weekend_plymouth2","","","","weekend_plymouth1","","","","","","","","","night_weekend","","weekend_torbay","night_weekend","","","","","","","","","","weekend_truro","","","",""
"Week 5 Day Sun","","","","night_weekend","","","","weekend_exeter","","","","","","","weekend_plymouth2","","","","weekend_plymouth1","","","","","","","","","night_weekend","","weekend_torbay","night_weekend","","","","","","","","","","weekend_truro","","","",""
"Week 6 Day Mon","","night_weekday","","","exeter_twilight","","","","night_weekday","","","","","","","","","","","","","plymouth_twilight","night_weekday","","","","torbay_twilight","","","","","","","","","","","truro_twilight","","","","","","",""
"Week 6 Day Tue","","night_weekday","","","","","exeter_twilight","","night_weekday","","","","","","","","","","","plymouth_twilight","","","night_weekday","","","","torbay_twilight","","","","","","","","","","truro_twilight","","","","","","","",""
"Week 6 Day Wed","","night_weekday","","","","","exeter_twilight","","night_weekday","","","","","","","","","","","","","","night_weekday","plymouth_twilight","","","torbay_twilight","","","","","","","","","truro_twilight","","","","","","","","",""
"Week 6 Day Thu","","night_weekday","","","","","","exeter_twilight","night_weekday","","","","","","","","","","","","","plymouth_twilight","night_weekday","","","","torbay_twilight","","","","","","","","","","","","","","","","","truro_twilight",""
"Week 6 Day Fri","night_weekend","","","","","weekend_exeter","","","","","night_weekend","plymouth_twilight","","","","","plymouth_bank_holidays","","","","night_weekend","","","","","","","","","","","","","weekend_torbay","","","weekend_truro","","","","","","","",""
"Week 6 Day Sat","night_weekend","","","","","weekend_exeter","","","","weekend_plymouth2","night_weekend","","","","","","","","","","night_weekend","","","weekend_plymouth1","","","","","","","","","","weekend_torbay","","","weekend_truro","","","","","","","",""
"Week 6 Day Sun","night_weekend","","","","","weekend_exeter","","","","weekend_plymouth2","night_weekend","","","","","","","","","","night_weekend","","","weekend_plymouth1","","","","","","","","","","weekend_torbay","","","weekend_truro","","","","","","","",""
"Week 7 Day Mon","","","exeter_twilight","","night_weekday","","","","","","","","","","","","","plymouth_bank_holidays","","plymouth_twilight","","","","","","","torbay_twilight","","","night_weekday","night_weekday","","","","","","","truro_twilight","","","","","","",""
"Week 7 Day Tue","","","","","night_weekday","","exeter_twilight","","","","","","","","","plymouth_twilight","","","","","","","","","","","","","","night_weekday","night_weekday","","","","torbay_twilight","","","truro_twilight","","","","","","",""
"Week 7 Day Wed","","","","","night_weekday","exeter_twilight","","","","","","","","","","","","plymouth_twilight","","","","","","","","","torbay_twilight","","","night_weekday","night_weekday","","","","","","","","","","truro_twilight","","","",""
"Week 7 Day Thu","","exeter_twilight","","","night_weekday","","","","","","plymouth_twilight","","","","","","","","","","","","","","","","torbay_twilight","","","night_weekday","night_weekday","","","","","","","","truro_twilight","","","","","",""
"Week 7 Day Fri","","","","","","","","","weekend_exeter","","","night_weekend","","","","","","","","","","night_weekend","","plymouth_twilight","","","","","weekend_torbay","","","","","","","","","","","weekend_truro","","","","night_weekend",""
"Week 7 Day Sat","","","","","","","","","weekend_exeter","","","night_weekend","","","","","weekend_plymouth1","","","weekend_plymouth2","","night_weekend","","","","","","","weekend_torbay","","","","","","","","","","","weekend_truro","","","","night_weekend",""
"Week 7 Day Sun","","","","","","","","","weekend_exeter","","","night_weekend","","","","","weekend_plymouth1","","","weekend_plymouth2","","night_weekend","","","","","","","weekend_torbay","","","","","","","","","","","weekend_truro","","","","night_weekend",""
"Week 8 Day Mon","night_weekday","","","exeter_twilight","","night_weekday","","","","","","","","night_weekday","plymouth_twilight","","","","","","","","","","","","","","","","torbay_twilight","","","","","","","","truro_twilight","","","","","",""
"Week 8 Day Tue","night_weekday","","","exeter_twilight","","night_weekday","","","","","","","","night_weekday","","","","","","","","","","plymouth_twilight","","","","","","","torbay_twilight","","","","","","","truro_twilight","","","","","","",""
"Week 8 Day Wed","night_weekday","","","","","night_weekday","","","exeter_twilight","","","","","night_weekday","","","","","","","","","","plymouth_twilight","","","","torbay_twilight","","","","","","","","","","","","","truro_twilight","","","",""
"Week 8 Day Thu","night_weekday","","","","exeter_twilight","night_weekday","","","","","","plymouth_twilight","","night_weekday","","","","","","","","","","","","","","torbay_twilight","","","","","","","","","","","","","truro_twilight","","","",""
"Week 8 Day Fri","","weekend_exeter","","","","","","night_weekend","","","night_weekend","plymouth_twilight","night_weekend","","","","","","","","","","","","","","","","","","","weekend_torbay","","","","","weekend_truro","","","","","","","",""
"Week 8 Day Sat","","weekend_exeter","","","","","","night_weekend","","","night_weekend","","night_weekend","","","","","","","","weekend_plymouth1","","","weekend_plymouth2","","","","","","","","weekend_torbay","","","","","weekend_truro","","","","","","","",""
"Week 8 Day Sun","","weekend_exeter","","","","","","night_weekend","","","night_weekend","","night_weekend","","","","","","","","weekend_plymouth1","","","weekend_plymouth2","","","","","","","","weekend_torbay","","","","","weekend_truro","","","","","","","",""
"Week 9 Day Mon","","","night_weekday","night_weekday","exeter_twilight","","","","","","","plymouth_twilight","","","night_weekday","","","","","","","","","","plymouth_bank_holidays","torbay_twilight","","","","","","","","","","","","","","truro_twilight","","","","",""
"Week 9 Day Tue","","","night_weekday","night_weekday","","","","","exeter_twilight","plymouth_twilight","","","","","night_weekday","","","","","","","","","","","","","","","","torbay_twilight","","","","","","","","","","truro_twilight","","","",""
"Week 9 Day Wed","","","night_weekday","night_weekday","","","","","exeter_twilight","","","","","","night_weekday","","","","","","","","","plymouth_twilight","","","","","","","torbay_twilight","","","","","","truro_twilight","","","","","","","",""
"Week 9 Day Thu","","","night_weekday","night_weekday","exeter_twilight","","","","","","","","","","night_weekday","","","","","","plymouth_twilight","","","","","","","","","","torbay_twilight","","","","","","","","","","","","","truro_twilight",""
"Week 9 Day Fri","","","","","","","weekend_exeter","","","night_weekend","","","","","","","","plymouth_twilight","","","","","","","","weekend_torbay","night_weekend","","","","","","","","","","","","","weekend_truro","night_weekend","","","",""
"Week 9 Day Sat","","","","","","","weekend_exeter","","","night_weekend","","","","","","","weekend_plymouth1","","weekend_plymouth2","","","","","","","weekend_torbay","night_weekend","","","","","","","","","","","","","weekend_truro","night_weekend","","","",""
"Week 9 Day Sun","","","","","","","weekend_exeter","","","night_weekend","","","","","","","weekend_plymouth1","","weekend_plymouth2","","","","","","","weekend_torbay","night_weekend","","","","","","","","","","","","","weekend_truro","night_weekend","","","",""
"Week 10 Day Mon","","","","","","","","exeter_twilight","","","","","night_weekday","","","","","plymouth_twilight","","","","","night_weekday","","","","","","","","torbay_twilight","night_weekday","","","","","","","","","","","truro_twilight","",""
"Week 10 Day Tue","","","exeter_twilight","","","","","","","","","","night_weekday","","","","","","","plymouth_twilight","","","night_weekday","","","","","","","","","night_weekday","","","torbay_twilight","truro_twilight","","","","","","","","",""
"Week 10 Day Wed","","exeter_twilight","","","","","","","","plymouth_twilight","","","night_weekday","","","","","","","","","","night_weekday","","","","","","","","","night_weekday","","","torbay_twilight","","","","","","","truro_twilight","","",""
"Week 10 Day Thu","","exeter_twilight","","","","","","","","","","","night_weekday","","","","","","","","","plymouth_twilight","night_weekday","","","","","","","","","night_weekday","","","torbay_twilight","","","","","","","","truro_twilight","",""
"Week 10 Day Fri","","","weekend_exeter","","","","","","","plymouth_twilight","","","","","","night_weekend","","","","","","","","","","","","","","","weekend_torbay","","","","","night_weekend","night_weekend","weekend_truro","","","","","","",""
"Week 10 Day Sat","","","weekend_exeter","","","","","","","","","weekend_plymouth1","","","","night_weekend","","weekend_plymouth2","","","","","","","","","","","","","weekend_torbay","","","","","night_weekend","night_weekend","weekend_truro","","","","","","",""
"Week 10 Day Sun","","","weekend_exeter","","","","","","","","","weekend_plymouth1","","","","night_weekend","","weekend_plymouth2","","","","","","","","","","","","","weekend_torbay","","","","","night_weekend","night_weekend","weekend_truro","","","","","","",""
"Week 11 Day Mon","","","","","","","exeter_twilight","","","","","","","","","","","","","","","plymouth_twilight","","night_weekday","","","","","","night_weekday","","torbay_twilight","","","","","","","night_weekday","","truro_twilight","","","",""
"Week 11 Day Tue","","exeter_twilight","","","","","","","","","","","","","plymouth_twilight","","","","","","","","","night_weekday","","","","","","night_weekday","","torbay_twilight","","","","","","","night_weekday","","","","","","truro_twilight"
"Week 11 Day Wed","","","exeter_twilight","","","","","","","","","","","","","","","","","plymouth_twilight","","","","night_weekday","","","","","","night_weekday","","torbay_twilight","","","","","","","night_weekday","","","truro_twilight","","",""
"Week 11 Day Thu","","exeter_twilight","","","","","","","","","","plymouth_twilight","","","","","","","","","","","","night_weekday","","","","","","night_weekday","","torbay_twilight","","","","","","","night_weekday","","","truro_twilight","","",""
"Week 11 Day Fri","","","","","weekend_exeter","","","","","","","","","","","","night_weekend","","","","night_weekend","plymouth_twilight","","","","","","weekend_torbay","","","","","","","","","","","","night_weekend","","","","weekend_truro",""
"Week 11 Day Sat","","","","","weekend_exeter","","","","","","weekend_plymouth2","","","","","","night_weekend","","weekend_plymouth1","","night_weekend","","","","","","","weekend_torbay","","","","","","","","","","","","night_weekend","","","","weekend_truro",""
"Week 11 Day Sun","","","","","weekend_exeter","","","","","","weekend_plymouth2","","","","","","night_weekend","","weekend_plymouth1","","night_weekend","","","","","","","weekend_torbay","","","","","","","","","","","","night_weekend","","","","weekend_truro",""
"Week 12 Day Mon","","","","","","","","exeter_twilight","","plymouth_twilight","","","","","","","","","","","","","","","","night_weekday","","","","torbay_twilight","","","","","","","","","truro_twilight","","night_weekday","","night_weekday","",""
"Week 12 Day Tue","","","","exeter_twilight","","","","","","","","","","","","","","plymouth_twilight","","","","","","","","night_weekday","","","","torbay_twilight","","","","","","","","truro_twilight","","","night_weekday","","night_weekday","",""
"Week 12 Day Wed","","","","","","","","","exeter_twilight","","","","","","","","","","plymouth_twilight","","","","","","","night_weekday","","","","torbay_twilight","","","","","","","","","","truro_twilight","night_weekday","","night_weekday","",""
"Week 12 Day Thu","","","","","","","","","exeter_twilight","","","","","","","","","","","","","","","","plymouth_twilight","night_weekday","","","","torbay_twilight","","","","","","","","","","","night_weekday","","night_weekday","","truro_twilight"
"Week 12 Day Fri","","night_weekend","","weekend_exeter","","","","","","","","night_weekend","","","","","","","","plymouth_twilight","","","","","","","","","","","","night_weekend","weekend_torbay","","","","","","weekend_truro","","","","","",""
"Week 12 Day Sat","","night_weekend","","weekend_exeter","","","","","","","","night_weekend","","weekend_plymouth1","","","","","","","","","weekend_plymouth2","","","","","","","","","night_weekend","weekend_torbay","","","","","","weekend_truro","","","","","",""
"Week 12 Day Sun","","night_weekend","","weekend_exeter","","","","","","","","night_weekend","","weekend_plymouth1","","","","","","","","","weekend_plymouth2","","","","","","","","","night_weekend","weekend_torbay","","","","","","weekend_truro","","","","","",""
"Week 13 Day Mon","","","","","","","exeter_twilight","","","","","","","","","","","","","plymouth_twilight","","","","","","torbay_twilight","night_weekday","","","","","","","","","","night_weekday","","","","","","truro_twilight","night_weekday",""
"Week 13 Day Tue","","","","","","","exeter_twilight","","","","","","","","","","","","","plymouth_twilight","","","","","","torbay_twilight","night_weekday","","","","","","","","","","night_weekday","","","","truro_twilight","","","night_weekday",""
"Week 13 Day Wed","","","","exeter_twilight","","","","","","","","plymouth_twilight","","","","","","","","","","","","","","","night_weekday","","","","","","torbay_twilight","","","","night_weekday","","","","","","","night_weekday","truro_twilight"
"Week 13 Day Thu","","exeter_twilight","","","","","","","","","","","","","","","","","","","plymouth_bank_holidays","","plymouth_twilight","","","","night_weekday","","","","","","torbay_twilight","","","","night_weekday","","","","","truro_twilight","","night_weekday",""
"Week 13 Day Fri","","","","","","weekend_exeter","","","night_weekend","","","","","","","","","night_weekend","plymouth_bank_holidays","night_weekend","","","plymouth_twilight","","","weekend_torbay","","","","","","","","","","","","","","","","","weekend_truro","",""
"Week 13 Day Sat","","","","","","weekend_exeter","","","night_weekend","","","","","","","weekend_plymouth2","","night_weekend","","night_weekend","","weekend_plymouth1","","","","weekend_torbay","","","","","","","","","","","","","","","","","weekend_truro","",""
"Week 13 Day Sun","","","","","","weekend_exeter","","","night_weekend","","","","","","","weekend_plymouth2","","night_weekend","","night_weekend","","weekend_plymouth1","","","","weekend_torbay","","","","","","","","","","","","","","","","","weekend_truro","",""
1 Name Madalina Drumea Layla Bell Alex wood Max Ireland Seren Peters Charles Finan Paul Ward Delilah Trimmer Tom Welsh Kyaw Tint Maththew O'Brien Max Finzel Zainab Sharaf Jean Skumar Michael Tomek Dhuvresh Patel Nicolas Dziadulewicz Vilim Kalamar Georgina Edwards Jenn Haw fong Paul Jenkins Amoolya Mannava Rebecca Murphy Ross Kruger Fiona Lyall Wijhdan Abusrewil Andrew MacCormick Joel Lim Mark Haley Alexander Sanchez - cabello Chong Yew Ng Matthew Thorley Sayed Hashim Alqarooni Harriet Conley Jon Skinner Alexander Wijnberg Cameron Bullock Alan Eccles Christian Greer Hannah Lewis Amina Odeh Sarath Vennam Mo Babsail Richard Chaytor Sherafghan PROC
2 Site barnstaple exeter exeter exeter exeter exeter exeter exeter exeter plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth plymouth_twilights torbay torbay torbay torbay torbay torbay torbay torbay torbay torbay twilights truro truro truro truro truro truro truro truro truro truro no proc
3 Grade 3 2 3 3 3 4 4 5 5 2 2 2 2 3 3 3 3 3 4 4 4 5 5 5 6 2 2 2 2 3 4 4 4 5 6 2 2 3 3 3 4 4 5 5 5
4 FTE 80 100 100 100 100 100 100 80 100 100 100 100 100 60 80 100 100 100 80 100 100 100 100 100 60 100 100 100 100 100 100 100 100 60 100 100 100 80 100 100 100 100 100 100 100
5 Week 1 Day Mon exeter_twilight night_weekday night_weekday plymouth_twilight torbay_twilight night_weekday truro_twilight
6 Week 1 Day Tue exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
7 Week 1 Day Wed exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
8 Week 1 Day Thu exeter_twilight night_weekday night_weekday plymouth_twilight torbay_twilight night_weekday truro_twilight
9 Week 1 Day Fri night_weekend weekend_exeter plymouth_twilight night_weekend weekend_torbay night_weekend weekend_truro
10 Week 1 Day Sat night_weekend weekend_exeter weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay night_weekend weekend_truro
11 Week 1 Day Sun night_weekend weekend_exeter weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay night_weekend weekend_truro
12 Week 2 Day Mon exeter_twilight plymouth_twilight night_weekday night_weekday night_weekday torbay_twilight truro_twilight
13 Week 2 Day Tue exeter_twilight plymouth_twilight night_weekday night_weekday night_weekday torbay_twilight truro_twilight
14 Week 2 Day Wed exeter_twilight night_weekday plymouth_twilight night_weekday night_weekday torbay_twilight truro_twilight
15 Week 2 Day Thu exeter_twilight night_weekday plymouth_twilight night_weekday night_weekday torbay_twilight truro_twilight
16 Week 2 Day Fri night_weekend weekend_exeter plymouth_twilight night_weekend weekend_torbay night_weekend weekend_truro
17 Week 2 Day Sat night_weekend weekend_exeter weekend_plymouth2 night_weekend weekend_plymouth1 weekend_torbay night_weekend weekend_truro
18 Week 2 Day Sun night_weekend weekend_exeter weekend_plymouth2 night_weekend weekend_plymouth1 weekend_torbay night_weekend weekend_truro
19 Week 3 Day Mon exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
20 Week 3 Day Tue exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
21 Week 3 Day Wed exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
22 Week 3 Day Thu exeter_twilight night_weekday night_weekday plymouth_twilight torbay_twilight night_weekday truro_twilight
23 Week 3 Day Fri weekend_exeter plymouth_twilight night_weekend night_weekend night_weekend weekend_torbay weekend_truro
24 Week 3 Day Sat weekend_exeter weekend_plymouth2 weekend_plymouth1 night_weekend night_weekend night_weekend weekend_torbay weekend_truro
25 Week 3 Day Sun weekend_exeter weekend_plymouth2 weekend_plymouth1 night_weekend night_weekend night_weekend weekend_torbay weekend_truro
26 Week 4 Day Mon exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight truro_twilight night_weekday
27 Week 4 Day Tue exeter_twilight night_weekday plymouth_twilight torbay_twilight night_weekday night_weekday truro_twilight
28 Week 4 Day Wed exeter_twilight night_weekday plymouth_twilight torbay_twilight night_weekday night_weekday truro_twilight
29 Week 4 Day Thu night_weekday exeter_twilight plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
30 Week 4 Day Fri weekend_exeter plymouth_twilight night_weekend weekend_torbay weekend_truro night_weekend night_weekend
31 Week 4 Day Sat weekend_exeter weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay weekend_truro night_weekend night_weekend
32 Week 4 Day Sun weekend_exeter weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay weekend_truro night_weekend night_weekend
33 Week 5 Day Mon exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
34 Week 5 Day Tue night_weekday exeter_twilight plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
35 Week 5 Day Wed night_weekday exeter_twilight plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
36 Week 5 Day Thu exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
37 Week 5 Day Fri night_weekend weekend_exeter plymouth_twilight night_weekend weekend_torbay night_weekend weekend_truro
38 Week 5 Day Sat night_weekend weekend_exeter weekend_plymouth2 weekend_plymouth1 night_weekend weekend_torbay night_weekend weekend_truro
39 Week 5 Day Sun night_weekend weekend_exeter weekend_plymouth2 weekend_plymouth1 night_weekend weekend_torbay night_weekend weekend_truro
40 Week 6 Day Mon night_weekday exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight truro_twilight
41 Week 6 Day Tue night_weekday exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight truro_twilight
42 Week 6 Day Wed night_weekday exeter_twilight night_weekday night_weekday plymouth_twilight torbay_twilight truro_twilight
43 Week 6 Day Thu night_weekday exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight truro_twilight
44 Week 6 Day Fri night_weekend weekend_exeter night_weekend plymouth_twilight plymouth_bank_holidays night_weekend weekend_torbay weekend_truro
45 Week 6 Day Sat night_weekend weekend_exeter weekend_plymouth2 night_weekend night_weekend weekend_plymouth1 weekend_torbay weekend_truro
46 Week 6 Day Sun night_weekend weekend_exeter weekend_plymouth2 night_weekend night_weekend weekend_plymouth1 weekend_torbay weekend_truro
47 Week 7 Day Mon exeter_twilight night_weekday plymouth_bank_holidays plymouth_twilight torbay_twilight night_weekday night_weekday truro_twilight
48 Week 7 Day Tue night_weekday exeter_twilight plymouth_twilight night_weekday night_weekday torbay_twilight truro_twilight
49 Week 7 Day Wed night_weekday exeter_twilight plymouth_twilight torbay_twilight night_weekday night_weekday truro_twilight
50 Week 7 Day Thu exeter_twilight night_weekday plymouth_twilight torbay_twilight night_weekday night_weekday truro_twilight
51 Week 7 Day Fri weekend_exeter night_weekend night_weekend plymouth_twilight weekend_torbay weekend_truro night_weekend
52 Week 7 Day Sat weekend_exeter night_weekend weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay weekend_truro night_weekend
53 Week 7 Day Sun weekend_exeter night_weekend weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay weekend_truro night_weekend
54 Week 8 Day Mon night_weekday exeter_twilight night_weekday night_weekday plymouth_twilight torbay_twilight truro_twilight
55 Week 8 Day Tue night_weekday exeter_twilight night_weekday night_weekday plymouth_twilight torbay_twilight truro_twilight
56 Week 8 Day Wed night_weekday night_weekday exeter_twilight night_weekday plymouth_twilight torbay_twilight truro_twilight
57 Week 8 Day Thu night_weekday exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight truro_twilight
58 Week 8 Day Fri weekend_exeter night_weekend night_weekend plymouth_twilight night_weekend weekend_torbay weekend_truro
59 Week 8 Day Sat weekend_exeter night_weekend night_weekend night_weekend weekend_plymouth1 weekend_plymouth2 weekend_torbay weekend_truro
60 Week 8 Day Sun weekend_exeter night_weekend night_weekend night_weekend weekend_plymouth1 weekend_plymouth2 weekend_torbay weekend_truro
61 Week 9 Day Mon night_weekday night_weekday exeter_twilight plymouth_twilight night_weekday plymouth_bank_holidays torbay_twilight truro_twilight
62 Week 9 Day Tue night_weekday night_weekday exeter_twilight plymouth_twilight night_weekday torbay_twilight truro_twilight
63 Week 9 Day Wed night_weekday night_weekday exeter_twilight night_weekday plymouth_twilight torbay_twilight truro_twilight
64 Week 9 Day Thu night_weekday night_weekday exeter_twilight night_weekday plymouth_twilight torbay_twilight truro_twilight
65 Week 9 Day Fri weekend_exeter night_weekend plymouth_twilight weekend_torbay night_weekend weekend_truro night_weekend
66 Week 9 Day Sat weekend_exeter night_weekend weekend_plymouth1 weekend_plymouth2 weekend_torbay night_weekend weekend_truro night_weekend
67 Week 9 Day Sun weekend_exeter night_weekend weekend_plymouth1 weekend_plymouth2 weekend_torbay night_weekend weekend_truro night_weekend
68 Week 10 Day Mon exeter_twilight night_weekday plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight
69 Week 10 Day Tue exeter_twilight night_weekday plymouth_twilight night_weekday night_weekday torbay_twilight truro_twilight
70 Week 10 Day Wed exeter_twilight plymouth_twilight night_weekday night_weekday night_weekday torbay_twilight truro_twilight
71 Week 10 Day Thu exeter_twilight night_weekday plymouth_twilight night_weekday night_weekday torbay_twilight truro_twilight
72 Week 10 Day Fri weekend_exeter plymouth_twilight night_weekend weekend_torbay night_weekend night_weekend weekend_truro
73 Week 10 Day Sat weekend_exeter weekend_plymouth1 night_weekend weekend_plymouth2 weekend_torbay night_weekend night_weekend weekend_truro
74 Week 10 Day Sun weekend_exeter weekend_plymouth1 night_weekend weekend_plymouth2 weekend_torbay night_weekend night_weekend weekend_truro
75 Week 11 Day Mon exeter_twilight plymouth_twilight night_weekday night_weekday torbay_twilight night_weekday truro_twilight
76 Week 11 Day Tue exeter_twilight plymouth_twilight night_weekday night_weekday torbay_twilight night_weekday truro_twilight
77 Week 11 Day Wed exeter_twilight plymouth_twilight night_weekday night_weekday torbay_twilight night_weekday truro_twilight
78 Week 11 Day Thu exeter_twilight plymouth_twilight night_weekday night_weekday torbay_twilight night_weekday truro_twilight
79 Week 11 Day Fri weekend_exeter night_weekend night_weekend plymouth_twilight weekend_torbay night_weekend weekend_truro
80 Week 11 Day Sat weekend_exeter weekend_plymouth2 night_weekend weekend_plymouth1 night_weekend weekend_torbay night_weekend weekend_truro
81 Week 11 Day Sun weekend_exeter weekend_plymouth2 night_weekend weekend_plymouth1 night_weekend weekend_torbay night_weekend weekend_truro
82 Week 12 Day Mon exeter_twilight plymouth_twilight night_weekday torbay_twilight truro_twilight night_weekday night_weekday
83 Week 12 Day Tue exeter_twilight plymouth_twilight night_weekday torbay_twilight truro_twilight night_weekday night_weekday
84 Week 12 Day Wed exeter_twilight plymouth_twilight night_weekday torbay_twilight truro_twilight night_weekday night_weekday
85 Week 12 Day Thu exeter_twilight plymouth_twilight night_weekday torbay_twilight night_weekday night_weekday truro_twilight
86 Week 12 Day Fri night_weekend weekend_exeter night_weekend plymouth_twilight night_weekend weekend_torbay weekend_truro
87 Week 12 Day Sat night_weekend weekend_exeter night_weekend weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay weekend_truro
88 Week 12 Day Sun night_weekend weekend_exeter night_weekend weekend_plymouth1 weekend_plymouth2 night_weekend weekend_torbay weekend_truro
89 Week 13 Day Mon exeter_twilight plymouth_twilight torbay_twilight night_weekday night_weekday truro_twilight night_weekday
90 Week 13 Day Tue exeter_twilight plymouth_twilight torbay_twilight night_weekday night_weekday truro_twilight night_weekday
91 Week 13 Day Wed exeter_twilight plymouth_twilight night_weekday torbay_twilight night_weekday night_weekday truro_twilight
92 Week 13 Day Thu exeter_twilight plymouth_bank_holidays plymouth_twilight night_weekday torbay_twilight night_weekday truro_twilight night_weekday
93 Week 13 Day Fri weekend_exeter night_weekend night_weekend plymouth_bank_holidays night_weekend plymouth_twilight weekend_torbay weekend_truro
94 Week 13 Day Sat weekend_exeter night_weekend weekend_plymouth2 night_weekend night_weekend weekend_plymouth1 weekend_torbay weekend_truro
95 Week 13 Day Sun weekend_exeter night_weekend weekend_plymouth2 night_weekend night_weekend weekend_plymouth1 weekend_torbay weekend_truro
+4
View File
@@ -318,3 +318,7 @@ table.transposed th.bank-holiday {
.target-assigned.no-colour {
background-color: white !important;
}
.locum-shift {
background-color: lightgreen;
}
+33 -1
View File
@@ -149,6 +149,8 @@ function generateExtra() {
bank_holidays_worked = bank_holidays.length;
nights_worked= $(tr).find(".night-shift").length;
shift_diff = JSON.parse(worker_td.attr("data-shift-diff"));
summed_shift_diff = Object.values(shift_diff).reduce((a, b) => a + b);
@@ -161,6 +163,7 @@ function generateExtra() {
worker_td.after(`<div class='worker-summary auto-generated'>
<span>Total shifts: ${total_shifts}, </span>
<span>Weekends: ${weekends_worked}, </span>
<span>Nights: ${nights_worked}, </span>
<span>Bank holidays: ${bank_holidays_worked}, </span>
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
@@ -280,6 +283,7 @@ $(".table-div .worker-row .worker").each((n, tr) => {
}
shift_counts = jtr.data("shift-counts")
locum_shift_counts = jtr.data("locum-shift-counts")
shift_targets = jtr.data("worker-targets")
oshifts.forEach((s) => {
@@ -289,7 +293,11 @@ $(".table-div .worker-row .worker").each((n, tr) => {
} else {
c = 0;
}
row.append(`<td class="target-assigned ${s}" data-assigned=${c} data-target=${shift_targets[s].toFixed(2)} data-diff=${shift_targets[s].toFixed(2)-c}>${c} (${shift_targets[s].toFixed(2)})</td>`)
locum_shifts = ""
if (s in locum_shift_counts) {
locum_shifts = `+${locum_shift_counts[s]}`
}
row.append(`<td class="target-assigned ${s}" data-assigned=${c} data-target=${shift_targets[s].toFixed(2)} data-diff=${shift_targets[s].toFixed(2)-c}>${c}${locum_shifts} (${shift_targets[s].toFixed(2)})</td>`)
} else {
row.append(`<td>-</td>`)
@@ -547,3 +555,27 @@ function hsv2rgb(h, s, v) {
return ("0" + Math.round(x * 255).toString(16)).slice(-2);
}).join('');
};
//$("#workers-json-target").append(syntaxHighlight(JSON.stringify(JSON.parse(($("#worker_details").data("workers"))))))
function syntaxHighlight(json) {
if (typeof json != 'string') {
json = JSON.stringify(json, undefined, 2);
}
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
+4
View File
@@ -0,0 +1,4 @@
# pytest.ini
[pytest]
testpaths = test
python_files = test_*.py
+9 -5
View File
@@ -4,10 +4,14 @@ pytest
black
rich
pydantic
fastapi
uvicorn
fastapi-sqlalchemy
alembic
psycopg2
#fastapi
#uvicorn
#fastapi-sqlalchemy
#alembic
#psycopg2
python-dotenv
requests
loguru
numpy
highspy
typer
+8
View File
@@ -0,0 +1,8 @@
# Load uk based bank holidays
from govuk_bank_holidays.bank_holidays import BankHolidays
bank_holidays = BankHolidays()
bank_holiday_map = {}
for bank_holiday in bank_holidays.get_holidays(division="england-and-wales"):
bank_holiday_map[bank_holiday["date"]] = bank_holiday["title"]
+804 -268
View File
File diff suppressed because it is too large Load Diff
+130 -49
View File
@@ -1,7 +1,7 @@
import datetime
from collections import defaultdict
from typing import Iterable, List, Literal, Optional
from pydantic import BaseModel, Extra, validator
from pydantic import BaseModel, ConfigDict, field_validator
from rich.pretty import pprint
from rota.console import console
@@ -32,12 +32,33 @@ class NotAvailableToWork(BaseModel):
reason: str = "unknown"
def generate_not_available_to_works(
start_date: datetime.date,
end_date: datetime.date | None = None,
days: int | None = None,
reason: str = "unknown",
) -> List[NotAvailableToWork]:
if end_date is None and days is None:
raise ValueError("Either end_date or days must be provided")
if end_date is not None and days is not None:
raise ValueError("Only one of end_date or days must be provided")
if end_date is not None:
days = (end_date - start_date).days
not_available_to_works = []
for day in range(days):
not_available_to_works.append(NotAvailableToWork(date=start_date + datetime.timedelta(days=day), reason=reason))
return not_available_to_works
class NonWorkingDays(BaseModel):
day: days
start_date: datetime.date | None = None
end_date: datetime.date | None = None
@validator("day")
@field_validator("day")
def day_in(cls, day):
for whole_day in whole_days:
if day.lower() in whole_day:
@@ -46,6 +67,9 @@ class NonWorkingDays(BaseModel):
class WorkRequests(BaseModel):
"""
Shift is the name of the shift requested, * is a wildcard for any shift
"""
date: datetime.date
shift: str = ""
@@ -66,9 +90,9 @@ class Worker(BaseModel):
site: str
# Grade are equivalent to roles, by keeping them integer defining model
# rules is easier.
grade: int
grade: int = 1
# We can either have a user generated ID
id: Optional[int] = None
id: Optional[int | uuid.UUID | str] = None
fte: int = 100
nwds: list[NonWorkingDays] = []
start_date: datetime.date | None = None
@@ -77,14 +101,22 @@ class Worker(BaseModel):
not_available_to_work: list[NotAvailableToWork] = []
pref_not_to_work: list[PreferenceNotToWork] = []
work_requests: list[WorkRequests] = []
locum_availability: list[WorkRequests] = []
locum_max_shifts: int = 0
locum_max_shifts_per_week: int = 0
locum_on_nwds: bool = False
remote_site: str = "plymouth" # We set a default proc_site
previous_shifts: dict = {}
shift_balance_extra: dict = {}
bank_holiday_extra: int = 0
pair: str | None = None
pair: int | str | None = None
locum: bool = False
class Config:
extra = Extra.allow
shift_fte_overrides: dict[str, int] = {} # Need checks to ensure shifts exist
model_config = ConfigDict(
extra="allow",
)
# def __init__(
# self,
@@ -108,30 +140,38 @@ class Worker(BaseModel):
#
# # days_to_work = Rota.rota_days_length
def load_rota(self, Rota):
self.proportion_rota_to_work = 1
self.shift_target_number = defaultdict(int)
def load_rota(self, Rota: "RotaBuilder"):
self.proportion_rota_to_work: float = 1
self.shift_target_number: dict[str, int] = defaultdict(int)
if self.id is None:
self.id = uuid.uuid4()
if self.start_date is not None and self.end_date is not None:
if self.start_date >= self.end_date:
raise ValueError("End date must be after start date")
if self.name == "Steph bailey":
print(self.start_date, self.end_date)
print(self.oop)
Rota.add_warning(
"Worker/Early end date",
f"{self.name} [{self.id}] end date is before rota start date: {self.end_date}",
)
raise ValueError(f"End date must be after start date: {self.name}")
# if no start date default to the start of the rota
if self.start_date is None:
self.calculated_start_date = Rota.start_date
else:
self.calculated_start_date = self.start_date
# ? test if start date is valid
if self.start_date < Rota.start_date:
self.calculated_start_date = self.start_date
# If worker start date is before rota start date use the rota start date
self.calculated_start_date = Rota.start_date
elif self.start_date >= Rota.rota_end_date:
# If it is after add a warning
Rota.add_warning(
"Worker/Late start date",
f"{self.name} [{self.id}] start date is after rota end date: {self.start_date}",
)
else:
self.calculated_start_date = self.start_date
pass
if self.end_date is None:
self.calculated_end_date = Rota.rota_end_date
@@ -149,15 +189,21 @@ class Worker(BaseModel):
if date < self.calculated_start_date:
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[
(self.id, week, day)
] = f"START DATE: {self.calculated_start_date}"
Rota.unavailable_to_work_reason[(self.id, week, day)] = (
f"START DATE: {self.calculated_start_date}"
)
if date >= self.calculated_end_date:
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[
(self.id, week, day)
] = f"END DATE: {self.calculated_end_date}"
Rota.unavailable_to_work_reason[(self.id, week, day)] = (
f"END DATE: {self.calculated_end_date}"
)
if not self.not_available_to_work:
Rota.add_warning(
"Worker/No unavailability",
f"{self.name} [{self.id}] has no unavailabilities (leave)",
)
for item in self.oop:
start_oop = item.start_date
@@ -201,15 +247,15 @@ class Worker(BaseModel):
]:
week, day = weeks_days
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[
(self.id, week, day)
] = f"OOP ({oop_name})".format(self.oop)
Rota.unavailable_to_work_reason[(self.id, week, day)] = (
f"OOP ({oop_name})".format(self.oop)
)
days_to_end = (self.calculated_end_date - Rota.start_date).days
# loop throught dates converting to week / day combination
for item in self.pref_not_to_work:
days_from_start = (item.date - Rota.start_date).days
for preference in self.pref_not_to_work:
days_from_start = (preference.date - Rota.start_date).days
# Ignore dates past the end of the rota (or end date)
if days_from_start < days_to_end:
week = days_from_start // 7 + 1
@@ -220,32 +266,44 @@ class Worker(BaseModel):
Rota.pref_not_to_work[(self.id, week, day)] = 1 / (
len(self.pref_not_to_work) + 1
)
Rota.pref_not_to_work_reason[(self.id, week, day)] = item.reason
Rota.pref_not_to_work_reason[(self.id, week, day)] = preference.reason
# print(not_available_to_work)
# loop throught dates converting to week / day combination
unavailable_set = set()
for item in self.not_available_to_work:
days_from_start = (item.date - Rota.start_date).days
for unavalability in self.not_available_to_work:
days_from_start = (unavalability.date - Rota.start_date).days
# Ignore dates past the end of the rota (or end date)
if days_from_start < days_to_end:
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
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)] = (
unavalability.reason
)
unavailable_set.add((week, day))
for item in self.work_requests:
days_from_start = (item.date - Rota.start_date).days
for request in self.work_requests:
days_from_start = (request.date - Rota.start_date).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
if item.shift == "*":
if request.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, request.shift))
for request in self.locum_availability:
days_from_start = (request.date - Rota.start_date).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
if request.shift == "*":
# We may want to add a limit to shifts that can be filled by locums?
for shift in Rota.get_shifts():
Rota.locum_availability.add((self.id, week, day, shift.name))
else:
Rota.locum_availability.add((self.id, week, day, request.shift))
# Calculate the proportion of the rota that is being worked
self.proportion_rota_to_work = days_to_work / Rota.rota_days_length
@@ -254,28 +312,45 @@ class Worker(BaseModel):
# We have to adjust the full time equivalent for people who CCT / leave the rota early
self.fte_adj = self.fte * self.proportion_rota_to_work
self.fte_adj_shifts = {}
if self.shift_fte_overrides:
for shift, fte in self.shift_fte_overrides.items():
self.fte_adj_shifts[shift] = fte * self.proportion_rota_to_work
if self.fte_adj > 100:
# Shouldn't happen ? bug if it does
Rota.add_warning(
"FTE > 100%", f"{self.name} [{self.id}] FTE = {self.fte_adj}"
)
raise ValueError("{} : fte_ajd = {}".format(self.name, self.fte_adj))
# TODO: this has already been validated, consider moving
self.non_working_day_list = []
for item in self.nwds:
for non_working_day in self.nwds:
start_date = Rota.start_date
end_date = Rota.rota_end_date
if item.start_date is not None:
start_date = item.start_date
if item.end_date is not None:
end_date = item.end_date
if non_working_day.start_date is not None:
start_date = non_working_day.start_date
if non_working_day.end_date is not None:
end_date = non_working_day.end_date
self.non_working_day_list.append((item.day, start_date, end_date))
self.non_working_day_list.append(
(non_working_day.day, start_date, end_date)
)
if days_to_work < 1:
self.fte_adj = 0
if days_to_work > 0 and (days_to_work - len(unavailable_set)) / days_to_work < 0.3:
if (
days_to_work > 0
and (days_to_work - len(unavailable_set)) / days_to_work < 0.3
):
console.print(f"{ self.name }: {days_to_work}, {Rota.rota_days_length}")
console.print(f"Warning {self.name} has excessive unavailability [{len(unavailable_set)}/{days_to_work}, adjusted fte={self.fte_adj}] (this may be infeasible)", style="red")
console.print(
f"Warning {self.name} has excessive unavailability [{len(unavailable_set)}/{days_to_work}, adjusted fte={self.fte_adj}] (this may be infeasible)",
style="red",
)
def __lt__(self, other) -> bool:
return (self.site, self.grade, self.fte_adj, self.name) < (
@@ -288,17 +363,17 @@ class Worker(BaseModel):
def __str__(self) -> str:
return f"{self.name}, {self.site}, {self.oop}"
#nwds = (
# 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.site, self.grade, self.fte_adj),
# nwds,
# self.remote_site,
#)
# )
def get_details(self) -> str:
return "{} {} {}".format(self.id, self.site[0], self.grade)
@@ -308,3 +383,9 @@ class Worker(BaseModel):
def get_shift_targets(self) -> dict:
return self.shift_target_number
def get_fte(self, shift: str | None=None) -> int:
if shift is not None:
if shift in self.fte_adj_shifts:
return self.fte_adj_shifts[shift]
return self.fte_adj
@@ -145,7 +145,7 @@ class TestWorkerRequests:
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"
assert Rota.results.solver.status in ("warning", "error")
def test_avoid_grade_constraint_multiple_shifts(self):
Rota = generate_basic_rota()
@@ -192,4 +192,4 @@ class TestWorkerRequests:
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"
assert Rota.results.solver.status in ("warning", "error")
@@ -146,7 +146,7 @@ class TestWorkerRequests:
Rota.add_worker_name_constraint_by_week(["worker3"], [1,2,3], ["a"])
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_avoid_worker_constraint_multiple_shifts(self):
Rota = generate_basic_rota()
@@ -193,4 +193,4 @@ class TestWorkerRequests:
Rota.add_worker_name_constraint_by_week(["worker1"], [7,8,9], ["a"])
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
+3 -3
View File
@@ -75,7 +75,7 @@ class TestBalancing:
name="a",
length=12.5,
days=days,
balance_weighting=5,
balance_weighting=10,
workers_required=1,
force_as_block=False,
constraint=[{"name": "pre","options": "2"}, {"name": "post","options": "2"}],
@@ -98,7 +98,7 @@ class TestBalancing:
),
)
Rota.build_and_solve(options={"ratio": 0.001})
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts")
shift_summary = Rota.get_shift_summary_dict()
@@ -198,7 +198,7 @@ class TestBalancing:
worker = Rota.get_worker_by_name(worker_name)
# assert worker_shifts["a"] in (46,47,48)
assert worker_shifts["c"] in (7, 8)
assert worker_shifts["c"] in (approx(7), approx(8))
#assert worker_shifts["b"] in (53, 54)
def test_weighted_shift_balancing4(self):
+3 -3
View File
@@ -146,7 +146,7 @@ class TestNightShifts:
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_nights2(self):
Rota = generate_basic_rota()
@@ -315,7 +315,7 @@ class TestNightShifts:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_nights_max_frequency3(self):
Rota = generate_basic_rota()
@@ -352,7 +352,7 @@ class TestNightShifts:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_nights_max_frequency4(self):
Rota = generate_basic_rota()
+5 -5
View File
@@ -90,7 +90,7 @@ class TestDemoRota:
self.Rota.export_rota_to_html("nwd")
assert self.Rota.results.solver.status == "warning"
assert self.Rota.results.solver.status in ("warning", "error")
def test_nwd_partial_rota(self):
# Set up rota
@@ -188,7 +188,7 @@ class TestDemoRota:
self.Rota.build_and_solve(options={"ratio": 0.00})
assert self.Rota.results.solver.status == "warning"
assert self.Rota.results.solver.status in ("warning", "error")
def test_nwd_force_as_block(self):
# Set up rota
@@ -273,21 +273,21 @@ class TestDemoRota:
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
#balance_offset=50,
workers_required=2,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
SingleShift(
sites=("group1",), name="w", length= 12.5, days=days[5:],
balance_offset=10,
#balance_offset=50,
workers_required=4,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.build_and_solve()
self.Rota.export_rota_to_html("nwd_block_force_split")
assert self.Rota.results.solver.status == "ok"
+1 -1
View File
@@ -40,7 +40,7 @@ class TestRemoteRotas:
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_remote_pass_week(self):
Rota = setup_basic_rota()
+31 -14
View File
@@ -2,7 +2,8 @@ from copy import deepcopy
from re import S
from black import main
import pytest
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
from pytest import approx
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, WarningTermination, days
import datetime
@@ -285,14 +286,14 @@ class TestDemoRotaNights:
worker = self.Rota.get_worker_by_name(worker_name)
if worker.fte == 40:
assert worker_shifts["night_weekday"] in (0, 4)
assert worker_shifts["night_weekend"] in (0, 3)
assert worker_shifts["night_weekday"] in (approx(0), approx(4))
assert worker_shifts["night_weekend"] in (approx(0), approx(3))
elif worker.fte == 60:
assert worker_shifts["night_weekday"] in (0, 4, 8)
assert worker_shifts["night_weekend"] in (0, 3, 6)
assert worker_shifts["night_weekday"] in (approx(0), approx(4), approx(8))
assert worker_shifts["night_weekend"] in (approx(0), approx(3), approx(6))
else:
assert worker_shifts["night_weekday"] in (4, 8)
assert worker_shifts["night_weekend"] in (3, 6)
assert worker_shifts["night_weekday"] in (approx(4), approx(8))
assert worker_shifts["night_weekend"] in (approx(3), approx(6))
class TestDemoRotaClear:
@@ -326,7 +327,7 @@ class TestDemoRotaClear:
name="a",
length=12.5,
days=days[:5],
balance_offset=40,
#balance_offset=40,
workers_required=2,
constraint=[{"name": "pre", "options": 1}],
),
@@ -335,7 +336,7 @@ class TestDemoRotaClear:
name="b",
length=12.5,
days=days[:5],
balance_offset=40,
#balance_offset=40,
workers_required=2,
),
SingleShift(
@@ -343,7 +344,7 @@ class TestDemoRotaClear:
name="c",
length=12.5,
days=days[3],
balance_offset=40,
#balance_offset=40,
workers_required=1,
constraint=[{"name": "pre", "options": 1}, {"name": "post", "options": 1}],
),
@@ -481,7 +482,7 @@ class TestDemoRotaShiftConstraints:
def test_max_shifts_fail(self):
self.Rota.constraint_options["max_shifts_per_month"] = 13
self.Rota.build_and_solve()
assert self.Rota.results.solver.status == "warning"
assert self.Rota.results.solver.status in ("warning", "error")
assert self.Rota.results.solver.termination_condition == "infeasible"
def test_max_shifts_extra_worker(self):
@@ -495,7 +496,7 @@ class TestDemoRotaShiftConstraints:
self.Rota.constraint_options["max_shifts_per_week"] = 3
self.Rota.build_and_solve()
assert self.Rota.results.solver.status == "warning"
assert self.Rota.results.solver.status in ("warning", "error")
assert self.Rota.results.solver.termination_condition == "infeasible"
def test_max_shifts_per_week_pass(self):
@@ -550,7 +551,17 @@ class TestDemoRotaBalanceShiftSites:
self.Rota.constraint_options["balance_nights_across_sites"] = False
self.Rota.constraint_options["balance_nights"] = False
self.Rota.constraint_options["constrain_time_off_after_nights"] = False
# Worker 5 and 6 don't have any valid shifts so this should fail
#with pytest.raises(WarningTermination):
# self.Rota.build_and_solve(options={"ratio": 0.0})
# Unless we remove the warning
self.Rota.terminate_on_warning.remove("Worker/no valid shifts")
self.Rota.build_and_solve(options={"ratio": 0.0})
assert len(self.Rota.get_warnings("Worker/no valid shifts")) == 2
self.Rota.export_rota_to_html("test5")
print(self.Rota.get_workers_by_group())
@@ -586,6 +597,12 @@ class TestDemoRotaBalanceShiftSites:
self.Rota.constraint_options["balance_nights_across_sites"] = False
self.Rota.constraint_options["balance_nights"] = False
self.Rota.constraint_options["constrain_time_off_after_nights"] = False
try:
self.Rota.terminate_on_warning.remove("Worker/no valid shifts")
except ValueError:
# Seems to happen with sequential test running
pass
self.Rota.build_and_solve(options={"ratio": 0.1})
group_workers = self.Rota.get_workers_by_group()
@@ -1151,7 +1168,7 @@ class TestNightUnavailable:
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("testnight_unavail")
assert self.Rota.results.solver.status == "warning"
assert self.Rota.results.solver.status in ("warning", "error")
assert self.Rota.results.solver.termination_condition == "infeasible"
def test_assign_non_night_prior_to_unavailablity(self):
@@ -1252,7 +1269,7 @@ class TestNightUnavailable:
#
# self.Rota.build_and_solve(options={"ratio": 0.00})
#
# assert self.Rota.results.solver.status == "warning"
# assert self.Rota.results.solver.status in ("warning", "error")
if __name__ == "__main__":
t = TestLimitConstraints()
+408 -11
View File
@@ -1,14 +1,13 @@
import datetime
import pytest
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, WarningTermination, days, WorkerRequirement
from rota.workers import Worker
from rota.workers import NotAvailableToWork, WorkRequests, Worker, generate_not_available_to_works
import itertools
def generate_basic_rota(weeks_to_rota=10):
start_date = datetime.date(2022, 3, 7)
def generate_basic_rota(weeks_to_rota=10, workers=2, start_date=datetime.date(2022, 3, 7)):
Rota = RotaBuilder(
start_date,
@@ -18,8 +17,7 @@ def generate_basic_rota(weeks_to_rota=10):
# Add a few workers
Rota.add_workers(
[
Worker(name="worker1", site="group1", grade=1),
Worker(name="worker2", site="group1", grade=1),
Worker(name=f"worker{i}", site="group1", grade=1) for i in range(1, workers+1)
]
)
@@ -142,7 +140,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("max_shifts_per_week_block")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_max_shifts_per_week_block_shift_number(self):
Rota = generate_basic_rota()
@@ -172,7 +170,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("max_shifts_per_week_block")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_pre_shift_constraint(self):
Rota = generate_basic_rota()
@@ -226,7 +224,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_pre_shift_constraint2(self):
Rota = generate_basic_rota()
@@ -269,7 +267,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("pre")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
def test_post_shift_constraint(self):
Rota = generate_basic_rota()
@@ -311,4 +309,403 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("post")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
class TestShiftDates:
def test_shift_start_date(self):
Rota = generate_basic_rota(workers=2)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
start_date=(Rota.start_date + datetime.timedelta(days=7)),
),
)
Rota.build_and_solve(options={"ratio": 0.000})
#Rota.export_rota_to_html("test_shift_start_date")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
assert Rota.get_workers_total_shifts()["worker1"] in (31, 32)
assert Rota.get_workers_total_shifts()["worker2"] in (31, 32)
def test_shift_start_date_end_date(self):
Rota = generate_basic_rota(workers=2)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
start_date=(Rota.start_date + datetime.timedelta(days=7)),
end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.000})
#Rota.export_rota_to_html("test_shift_start_date")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
assert Rota.get_workers_total_shifts()["worker1"] in (17, 18)
assert Rota.get_workers_total_shifts()["worker2"] in (17, 18)
for worker in Rota.get_workers():
assert Rota.get_worker_shift_list_string(worker).startswith("-"*7)
assert Rota.get_worker_shift_list_string(worker).endswith("-"*28)
def test_shift_invalid_start_date(self):
Rota = generate_basic_rota(workers=2)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
start_date=(Rota.start_date - datetime.timedelta(days=6)),
),
)
with pytest.raises(WarningTermination):
Rota.build_and_solve(options={"ratio": 0.000})
def test_shift_invalid_start_date2(self):
Rota = generate_basic_rota(workers=2)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
start_date=(Rota.rota_end_date + datetime.timedelta(days=1)),
),
)
with pytest.raises(WarningTermination):
Rota.build_and_solve(options={"ratio": 0.000})
def test_shift_invalid_end_date(self):
Rota = generate_basic_rota(workers=2)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
end_date=(Rota.rota_end_date + datetime.timedelta(days=1)),
),
)
with pytest.raises(WarningTermination):
Rota.build_and_solve(options={"ratio": 0.000})
def test_shift_invalid_end_date2(self):
Rota = generate_basic_rota(workers=2)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
end_date=(Rota.start_date - datetime.timedelta(days=1)),
),
)
with pytest.raises(WarningTermination):
Rota.build_and_solve(options={"ratio": 0.000})
def test_shift_start_date_end_date_block(self):
Rota = generate_basic_rota(workers=2)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days,
assign_as_block=True,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
start_date=(Rota.start_date + datetime.timedelta(days=7)),
end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.000})
#Rota.export_rota_to_html("test_shift_start_date")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
assert Rota.get_workers_total_shifts()["worker1"] in (17, 18)
assert Rota.get_workers_total_shifts()["worker2"] in (17, 18)
for worker in Rota.get_workers():
assert Rota.get_worker_shift_list_string(worker).startswith("-"*7)
assert Rota.get_worker_shift_list_string(worker).endswith("-"*28)
def test_shift_start_date_end_date_block_more_workers(self):
Rota = generate_basic_rota(workers=10)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
assign_as_block=True,
force_as_block=True,
workers_required=4,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
SingleShift(
sites=("group1", "group2"), name="b", length= 12.5, days=days[4:],
assign_as_block=True,
force_as_block=True,
workers_required=4,
constraint=[{"name": "max_shifts_per_week", "options": 4},
{"name": "pre", "options": 3}, {"name": "post", "options": 3}],
start_date=(Rota.start_date + datetime.timedelta(days=7)),
end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("test_shift_start_date")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
print(Rota.get_worker_shift_list_string(worker))
assert total_shifts[worker.name] == 22
assert "ab" not in Rota.get_worker_shift_list_string(worker)
assert "ba" not in Rota.get_worker_shift_list_string(worker)
def test_shift_start_date_end_date_block_with_requests(self):
Rota = generate_basic_rota(workers=7)
request_date = Rota.start_date + datetime.timedelta(days=(7*6)+4)
request = [WorkRequests(date=request_date, shift="c")]
Rota.add_workers(
[
Worker(name=f"worker-wr-{i}", site="group2", grade=1, work_requests=request) for i in range(1, 4)
#Worker(name="worker2", site="group1", grade=1),
]
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
assign_as_block=True,
force_as_block=True,
workers_required=4,
constraint=[{"name": "max_shifts_per_week", "options": 4}],
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
SingleShift(
sites=("group1", "group2"), name="b", length= 12.5, days=days[4:],
assign_as_block=True,
force_as_block=True,
workers_required=4,
constraint=[{"name": "max_shifts_per_week", "options": 4},
{"name": "pre", "options": 3}, {"name": "post", "options": 3}],
start_date=(Rota.start_date + datetime.timedelta(days=7)),
end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
SingleShift(
sites=("group1", "group2"), name="c", length= 12.5, days=days[4:],
assign_as_block=True,
force_as_block=True,
workers_required=4,
constraint=[{"name": "max_shifts_per_week", "options": 4},
{"name": "pre", "options": 3}, {"name": "post", "options": 3}],
start_date=(Rota.start_date + datetime.timedelta(days=(7*6))),
),
)
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test_shift_start_date")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
print(Rota.get_worker_shift_list_string(worker))
assert total_shifts[worker.name] in range(24,30)
shift_string = Rota.get_worker_shift_list_string(worker)
assert "ab" not in shift_string
assert "ba" not in shift_string
# Check all has been assigned as block
assert shift_string.count("aaaa") == shift_string.count("a") / 4
assert shift_string.count("bbb") == shift_string.count("b") / 3
assert shift_string.count("ccc") == shift_string.count("c") / 3
# TODO test the request assignment
for worker in Rota.get_workers_by_group()["group2"]:
assert Rota.get_worker_shifts_by_date(worker)[request_date] == "c"
class TestShiftWorkerRequirements:
def test_start_and_end_date(self):
WorkerRequirement(start_date=datetime.date(2022, 3, 7), end_date=datetime.date(2022, 3, 14))
def test_end_date_before_start_date(self):
with pytest.raises(ValueError):
WorkerRequirement(start_date=datetime.date(2022, 3, 14), end_date=datetime.date(2022, 3, 7))
def test_worker_requirement(self):
Rota = generate_basic_rota(workers=2)
wr = [WorkerRequirement(start_date=datetime.date(2022, 3, 7), end_date=datetime.date(2022, 3, 21))]
print(wr)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
assign_as_block=True,
force_as_block=True,
workers_required=wr,
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test_worker_requirement")
assert Rota.results.solver.status == "ok"
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
assert total_shifts[worker.name] == 4
assert "aaaa" in Rota.get_worker_shift_list_string(worker)
def test_worker_requirement2(self):
Rota = generate_basic_rota(workers=2)
wr = [WorkerRequirement(start_date=datetime.date(2022, 3, 7), end_date=datetime.date(2022, 3, 21)),
WorkerRequirement(start_date=datetime.date(2022, 3, 28))
]
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
assign_as_block=True,
#force_as_block=True,
workers_required=wr,
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test_worker_requirement")
assert Rota.results.solver.status == "ok"
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
assert total_shifts[worker.name] == 18
assert Rota.get_worker_shift_list_string(worker)[14:21] == "-------"
def test_worker_requirement_increase_workers(self):
Rota = generate_basic_rota(workers=2)
wr = [WorkerRequirement(start_date=datetime.date(2022, 3, 7), end_date=datetime.date(2022, 3, 14)),
WorkerRequirement(start_date=datetime.date(2022, 3, 28), number=2)
]
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
assign_as_block=True,
#force_as_block=True,
workers_required=wr,
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test_worker_requirement")
assert Rota.results.solver.status == "ok"
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
assert total_shifts[worker.name] == 30
assert Rota.get_worker_shift_list_string(worker)[7:21] == "-" * 14
assert Rota.get_worker_shift_list_string(worker).endswith("aaaa---"*7)
def test_worker_requirement_balancing(self):
Rota = generate_basic_rota(workers=2)
natws = generate_not_available_to_works(datetime.date(2022,4,18), days=21, reason="holiday")
Rota.add_worker(
Worker(name=f"worker3", site="group1", grade=1, not_available_to_work=natws)
)
wr = [WorkerRequirement(start_date=datetime.date(2022, 3, 7), end_date=datetime.date(2022, 3, 14)),
WorkerRequirement(start_date=datetime.date(2022, 3, 28), number=2)
]
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
assign_as_block=True,
workers_required=wr,
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.0})
Rota.export_rota_to_html("test_worker_requirement")
assert Rota.results.solver.status == "ok"
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
assert total_shifts[worker.name] == 20
assert Rota.get_worker_shift_list_string(worker)[7:21] == "-" * 14
if worker.name == "worker3":
assert Rota.get_worker_shift_list_string(worker).endswith("aaaa---")
assert Rota.get_worker_shift_list_string(worker).startswith("aaaa---")
def test_worker_requirement_multiple_shifts(self):
Rota = generate_basic_rota(workers=4)
wr = [WorkerRequirement(start_date=datetime.date(2022, 3, 7), end_date=datetime.date(2022, 3, 14)),
WorkerRequirement(start_date=datetime.date(2022, 3, 28), number=2)
]
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"), name="b", length= 12.5, days=days[4:],
assign_as_block=True,
#force_as_block=True,
workers_required=2,
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
SingleShift(
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
assign_as_block=True,
#force_as_block=True,
workers_required=wr,
#start_date=(Rota.start_date + datetime.timedelta(days=7)),
#end_date=(Rota.start_date + datetime.timedelta(days=((7*6)-1))),
),
)
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test_worker_requirement")
assert Rota.results.solver.status == "ok"
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
assert total_shifts[worker.name] == 30
+2 -2
View File
@@ -210,7 +210,7 @@ class TestDemoRota:
self.Rota.build_and_solve(options={"ratio": 0.00})
assert self.Rota.results.solver.status == "warning"
assert self.Rota.results.solver.status in ("warning", "error")
def test_post_mon(self):
# Set up rota
@@ -343,4 +343,4 @@ class TestDemoRota:
self.Rota.export_rota_to_html("weekend_padding")
assert self.Rota.results.solver.status == "warning"
assert self.Rota.results.solver.status in ("warning", "error")
+6 -5
View File
@@ -1,5 +1,6 @@
import datetime
import pytest
from pytest import approx
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days
from rota.workers import Worker
@@ -94,7 +95,7 @@ class TestWorkerRequests:
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("preferences_not_to_work2")
Rota.export_rota_to_html("preferences_not_to_work2", timestamp_filename=False)
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
@@ -179,7 +180,7 @@ class TestWorkerRequests:
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (46, 47)
assert worker_shifts["a"] in (approx(46), approx(47))
# shift_string = Rota.get_worker_shift_list_string(worker)
@@ -216,7 +217,7 @@ class TestWorkerRequests:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("unavailable_to_work2")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
assert Rota.results.solver.termination_condition == "infeasible"
def test_unavailable_to_work3(self):
@@ -304,7 +305,7 @@ class TestWorkerRequests:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("unavailable_to_work4")
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
assert Rota.results.solver.termination_condition == "infeasible"
def test_work_requests(self):
@@ -523,7 +524,7 @@ class TestWorkerRequests:
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (26, 27)
assert worker_shifts["b"] == 10
assert worker_shifts["b"] == approx(10)
shift_string = Rota.get_worker_shift_list_string(worker)
+542 -16
View File
@@ -1,24 +1,35 @@
import pytest
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, WarningTermination, WorkerRequirement, days
import datetime
from rota.workers import Worker
from rota.workers import NonWorkingDays, NotAvailableToWork, Worker, generate_not_available_to_works
from rota.workers import WorkRequests
def setup_basic_rota():
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
return RotaBuilder(
def generate_basic_rota(
weeks_to_rota=10, workers=0, start_date=datetime.date(2022, 3, 7)
):
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
# Add a few workers
if workers:
Rota.add_workers(
[
Worker(name=f"worker{i:02}", site="group1", grade=1)
for i in range(1, workers + 1)
]
)
return Rota
class TestWorkers:
def test_no_workers(self):
"""Check error raised if attempt to solve with no workers"""
Rota = setup_basic_rota()
Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
@@ -42,8 +53,7 @@ class TestWorkers:
Rota.build_and_solve(solve=False)
def test_workers_duplicate_name(self):
Rota = setup_basic_rota()
Rota = generate_basic_rota()
workers = []
workers.append(Worker(name="worker1", site="group1", grade=1, fte=100))
@@ -64,11 +74,10 @@ class TestWorkers:
),
)
with pytest.raises(ValueError):
with pytest.raises(WarningTermination):
Rota.build_and_solve(solve=False)
def test_worker_ftes(self):
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
@@ -143,11 +152,14 @@ class TestWorkers:
),
)
with pytest.raises(WarningTermination):
Rota.build_and_solve()
Rota.terminate_on_warning.remove("Worker/no valid shifts")
Rota.build_and_solve()
# All workers should have an adjusted fte of 50 %
for worker in Rota.get_workers():
# Except those in group3
if worker.site == "group3":
assert worker.fte_adj == 0
@@ -298,6 +310,7 @@ class TestWorkers:
),
)
Rota.terminate_on_warning.remove("Worker/no valid shifts")
Rota.build_and_solve()
# All workers should have an adjusted fte of 50 %
@@ -320,7 +333,7 @@ class TestWorkers:
Rota.export_rota_to_html("worker_oops")
def test_worker_pairs(self):
Rota = setup_basic_rota()
Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
@@ -353,7 +366,7 @@ class TestWorkers:
Rota.build_and_solve()
# With two paired workers we shouldn't be able to solve
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
worker3 = Worker(name="worker3", site="group1", grade=1, fte=100)
Rota.add_worker(worker3)
@@ -374,7 +387,7 @@ class TestWorkers:
Rota.build_and_solve()
assert Rota.results.solver.status == "warning"
assert Rota.results.solver.status in ("warning", "error")
worker3.pair = 0
@@ -391,3 +404,516 @@ class TestWorkers:
assert Rota.results.solver.status == "ok"
assert Rota.get_workers_total_shifts()["worker3"] == 50
def test_not_available_to_work(self):
Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
sites=("group1",),
name="a",
length=12.5,
days=days,
workers_required=1,
),
)
natws = generate_not_available_to_works(Rota.start_date, days=21)
natws.append(
NotAvailableToWork(date=Rota.rota_end_date - datetime.timedelta(days=1))
)
Rota.add_workers(
[
Worker(name="worker1", site="group1", grade=1),
Worker(
name="worker2", site="group1", grade=1, not_available_to_work=natws
),
]
)
Rota.build_and_solve()
Rota.export_rota_to_html("test_not_available_to_work")
assert Rota.results.solver.status == "ok"
assert Rota.get_workers_total_shifts()["worker1"] == 35
assert Rota.get_workers_total_shifts()["worker2"] == 35
assert Rota.get_worker_shift_list_string(
Rota.get_worker_by_name("worker2")
).startswith("-" * 21)
assert Rota.get_worker_shift_list_string(
Rota.get_worker_by_name("worker2")
).endswith("-")
class TestWorkerFtes:
def test_worker_ftes(self):
Rota = generate_basic_rota(workers=2)
Rota.add_workers(
[
Worker(
name="worker3",
site="group1",
grade=1,
fte=60,
),
Worker(
name="worker4",
site="group1",
grade=1,
fte=40,
),
]
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:6],
workers_required=1,
),
)
Rota.build_and_solve()
Rota.export_rota_to_html("test_worker_ftes")
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
match worker.name:
case "worker1":
assert worker.get_fte() == 100
assert total_shifts[worker.name] == 20
case "worker2":
assert worker.get_fte() == 100
assert total_shifts[worker.name] == 20
case "worker3":
assert worker.get_fte() == 60
assert total_shifts[worker.name] == 12
case "worker4":
assert worker.get_fte() == 40
assert total_shifts[worker.name] == 8
def test_worker_fte_shift_override(self):
Rota = generate_basic_rota(workers=1)
Rota.add_workers(
[
Worker(
name="worker2",
site="group1",
grade=1,
fte=100,
shift_fte_overrides={"b": 50}
),
]
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:2],
workers_required=1,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[2:5],
workers_required=1,
),
)
Rota.build_and_solve()
Rota.export_rota_to_html("test_worker_fte_shift_override")
total_shifts = Rota.get_workers_total_shifts()
for worker in Rota.get_workers():
match worker.name:
case "worker1":
assert worker.get_fte() == 100
assert total_shifts[worker.name] == 30
assert worker.get_fte("b") == 100
case "worker2":
assert worker.get_fte() == 100
assert worker.get_fte("b") == 50
assert total_shifts[worker.name] == 20
class TestComplexWorkers:
def test_worker_requirement_and_force_block(self):
Rota = generate_basic_rota(workers=10, weeks_to_rota=10)
wr = [
WorkerRequirement(
end_date=Rota.start_date + datetime.timedelta(weeks=5),
number=2,
),
WorkerRequirement(
start_date=Rota.start_date + datetime.timedelta(weeks=5),
number=3,
),
]
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:2],
workers_required=1,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[2:5],
workers_required=1,
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[:4],
workers_required=wr,
force_as_block=True,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
]
),
SingleShift(
sites=("group1", "group2"),
name="d",
length=12.5,
days=days[4:],
workers_required=wr,
force_as_block=True,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
]
),
)
Rota.build_and_solve()
Rota.export_rota_to_html("test_worker_complex")
#total_shifts = Rota.get_workers_total_shifts()
#for worker in Rota.get_workers():
# match worker.name:
# case "worker1":
# assert worker.get_fte() == 100
# assert total_shifts[worker.name] == 30
# assert worker.get_fte("b") == 100
# case "worker2":
# assert worker.get_fte() == 100
# assert worker.get_fte("b") == 50
# assert total_shifts[worker.name] == 20
#total_shifts = Rota.get_workers_total_shifts()
assert Rota.results.solver.status == "ok"
for worker in Rota.get_workers():
shift_string = Rota.get_worker_shift_list_string(worker)
assert shift_string.count("cccc") == shift_string.count("c") / 4
assert shift_string.count("ddd") == shift_string.count("d") / 3
def test_split_shift(self):
Rota = generate_basic_rota(workers=11, weeks_to_rota=22)
Rota.set_rota_constraint("max_shifts_per_week", 2)
Rota.set_rota_constraint("max_weekend_frequency", 3)
Rota.set_rota_constraint("max_shifts_per_month", 4)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:4],
workers_required=1,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
{"name": "max_shifts_per_week", "options": 1},
]
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=(days[4], days[6]),
workers_required=1,
assign_as_block=True,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
]
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[5],
workers_required=1,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
]
),
)
Rota.build_and_solve()
Rota.export_rota_to_html("test_split")
assert Rota.results.solver.status == "ok"
class TestLocums:
def test_locums_basic(self):
Rota = generate_basic_rota(workers=2, weeks_to_rota=6)
la = []
for date in Rota.get_date_range(end_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
#for date in Rota.get_date_range():
la.append(WorkRequests(date=date, shift="*"))
la2 = []
for date in Rota.get_date_range(start_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
#for date in Rota.get_date_range():
la2.append(WorkRequests(date=date, shift="*"))
Rota.add_workers(
[
Worker(name="worker03", site="group1", locum=False, locum_availability=la, locum_max_shifts=40, locum_max_shifts_per_week=1),
Worker(name="worker04", site="group1", locum=False, locum_availability=la2, locum_max_shifts=40, locum_max_shifts_per_week=2),
Worker(name="locum1", site="group1", locum=True),
Worker(
name="locum2", site="group1", locum=True
),
]
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:4],
workers_required=1,
constraint=[
#{"name": "pre", "options": 1},
#{"name": "post", "options": 1},
#{"name": "max_shifts_per_week", "options": 1},
]
),
)
Rota.set_rota_constraint("distribute_locum_shifts", True)
Rota.set_rota_constraint("maximum_allowed_shift_diff", 0)
Rota.set_rota_constraint("maximum_allowed_locum_shifts_per_worker", 4)
Rota.build_and_solve()
Rota.export_rota_to_html("test_locums_basic", timestamp_filename=False)
assert Rota.results.solver.status == "ok"
for worker in Rota.get_workers():
assert Rota.get_worker_shift_list(worker).count("a") == 4
def test_locums_nwds(self):
Rota = generate_basic_rota(workers=2, weeks_to_rota=6)
la = []
for date in Rota.get_date_range(end_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
la.append(WorkRequests(date=date, shift="*"))
la2 = []
for date in Rota.get_date_range(start_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
la2.append(WorkRequests(date=date, shift="*"))
Rota.add_workers(
[
Worker(name="worker03", site="group1", locum=False, locum_availability=la, locum_max_shifts=40, locum_max_shifts_per_week=1, nwds=[NonWorkingDays(day="Tue")]),
Worker(name="worker04", site="group1", locum=False, locum_availability=la2, locum_max_shifts=40, locum_max_shifts_per_week=2, nwds=[NonWorkingDays(day="Wed"),NonWorkingDays(day="Thu")]),
#Worker(name="worker03", site="group1", locum=False, locum_availability=la, locum_max_shifts=40, locum_max_shifts_per_week=1, nwds=[NonWorkingDays(day="Mon"),NonWorkingDays(day="Tue")]),
#Worker(name="worker04", site="group1", locum=False, locum_availability=la2, locum_max_shifts=40, locum_max_shifts_per_week=2, nwds=[NonWorkingDays(day="Wed"),NonWorkingDays(day="Thu")]),
Worker(name="locum1", site="group1", locum=True),
Worker(
name="locum2", site="group1", locum=True
),
]
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:4],
workers_required=1,
constraint=[
#{"name": "pre", "options": 1},
#{"name": "post", "options": 1},
#{"name": "max_shifts_per_week", "options": 1},
]
),
)
#Rota.set_rota_constraint("distribute_locum_shifts", True)
Rota.set_rota_constraint("maximum_allowed_shift_diff", 0)
Rota.set_rota_constraint("maximum_allowed_locum_shifts_per_worker", 4)
#Rota.set_rota_constraint("minimum_allowed_locum_shifts_per_worker", 3)
Rota.build_and_solve()
Rota.export_rota_to_html("test_locums_basic", timestamp_filename=False)
assert Rota.results.solver.status == "ok"
for worker in Rota.get_workers():
print( Rota.get_worker_shift_list(worker))
assert Rota.get_worker_shift_list(worker).count("a") == 4
if worker.name in ("worker03", "worker04"):
assert Rota.get_worker_shift_list(worker, include_locums=True).count("a") > 6
def test_locums_no_availablity(self):
Rota = generate_basic_rota(workers=2, weeks_to_rota=5)
la = []
#for date in Rota.get_date_range(end_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
# la.append(WorkRequests(date=date, shift="*"))
la2 = []
#for date in Rota.get_date_range(start_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
# la2.append(WorkRequests(date=date, shift="*"))
Rota.add_workers(
[
Worker(name="worker03", site="group1", locum=False, locum_availability=la, locum_max_shifts=40, locum_max_shifts_per_week=1),
Worker(name="worker04", site="group1", locum=False, locum_availability=la2, locum_max_shifts=40, locum_max_shifts_per_week=2),
Worker(name="locum1", site="group1", locum=True),
Worker(
name="locum2", site="group1", locum=True
),
]
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:4],
workers_required=1,
constraint=[
#{"name": "pre", "options": 1},
#{"name": "post", "options": 1},
#{"name": "max_shifts_per_week", "options": 1},
]
),
)
Rota.set_rota_constraint("distribute_locum_shifts", False)
with pytest.raises(WarningTermination):
Rota.build_and_solve()
def test_locums(self):
Rota = generate_basic_rota(workers=7, weeks_to_rota=10)
Rota.set_rota_constraint("max_shifts_per_week", 2)
Rota.set_rota_constraint("max_weekend_frequency", 3)
Rota.set_rota_constraint("max_shifts_per_month", 4)
#Rota.set_rota_constraint("balance_shifts", False)
Rota.set_rota_constraint("distribute_locum_shifts", True)
Rota.set_rota_constraint("maximum_allowed_shift_diff", 1)
Rota.set_rota_constraint("maximum_allowed_locum_shifts_per_worker", 6)
Rota.terminate_on_warning.remove("Worker/no valid shifts")
la = []
for date in Rota.get_date_range(end_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
la.append(WorkRequests(date=date, shift="*"))
la2 = []
for date in Rota.get_date_range(start_date=Rota.rota_end_date-datetime.timedelta(weeks=3)):
la2.append(WorkRequests(date=date, shift="*"))
Rota.add_workers(
[
Worker(name="worker08", site="group1", locum=False, locum_availability=la, locum_max_shifts=40, locum_max_shifts_per_week=1),
Worker(name="worker09", site="group1", locum=False, locum_availability=la2, locum_max_shifts=40, locum_max_shifts_per_week=2),
Worker(name="locum1", site="group1", locum=True),
Worker(
name="locum2", site="group1", locum=True
),
#Worker(
# name="extra1", site="locum", locum_availability=la,
# locum_max_shifts=40
#),
#Worker(
# name="extra2", site="locum", locum_availability=la,
# locum_max_shifts=4, locum_max_shifts_per_week=1
#),
]
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:4],
workers_required=1,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
{"name": "max_shifts_per_week", "options": 1},
]
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=(days[4], days[6]),
workers_required=1,
assign_as_block=True,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
]
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[5],
workers_required=1,
constraint=[
{"name": "pre", "options": 2},
{"name": "post", "options": 3},
]
),
)
Rota.build_and_solve()
Rota.export_rota_to_html("test_locums", timestamp_filename=False)
assert Rota.results.solver.status == "ok"