Compare commits
11
Commits
8c28e7155a
...
242a664f49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
242a664f49 | ||
|
|
8459a4b3a9 | ||
|
|
7ce4677921 | ||
|
|
9af01a3415 | ||
|
|
f0b9ee0ebc | ||
|
|
1ed5f4fbcd | ||
|
|
70dc279a02 | ||
|
|
51b4e89601 | ||
|
|
44c21e3c6a | ||
|
|
a7e1fa7171 | ||
|
|
5d55ed77a8 |
@@ -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,25 +31,28 @@ 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
|
||||
|
||||
|
||||
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)
|
||||
@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(
|
||||
start_date, weeks_to_rota=26, balance_offset_modifier=1, name="proc rota"
|
||||
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"] = []
|
||||
@@ -51,9 +60,25 @@ Rota.constraint_options["max_weekend_frequency"] = 2
|
||||
Rota.constraint_options["hard_constrain_pair_separation"] = True
|
||||
# Rota.constraint_options["avoid_st2_first_month"] = True
|
||||
|
||||
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.add_shifts(
|
||||
SingleShift(
|
||||
sites=("exeter", "exeter twilights and weekends", "exeter no nights"),
|
||||
sites=(
|
||||
"exeter",
|
||||
"exeter twilights and weekends",
|
||||
"exeter no nights",
|
||||
"exeter twilights",
|
||||
),
|
||||
name="exeter_twilight",
|
||||
length=12.5,
|
||||
days=days[:5],
|
||||
@@ -66,14 +91,21 @@ Rota.add_shifts(
|
||||
],
|
||||
),
|
||||
SingleShift(
|
||||
sites=("truro", "truro twilights"),
|
||||
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"),
|
||||
sites=("torbay", "torbay twilights", "torbay twilights and weekends"),
|
||||
name="torbay_twilight",
|
||||
length=12.5,
|
||||
days=days[:4],
|
||||
@@ -81,7 +113,12 @@ Rota.add_shifts(
|
||||
assign_as_block=True,
|
||||
),
|
||||
SingleShift(
|
||||
sites=("plymouth", "plymouth twilights", "plymouth twilights and weekends"),
|
||||
sites=(
|
||||
"plymouth",
|
||||
"plymouth twilights",
|
||||
"plymouth twilights and weekends",
|
||||
"plymouth twilights, weekends and weekend nights",
|
||||
),
|
||||
name="plymouth_twilight",
|
||||
length=12.5,
|
||||
days=days[:5],
|
||||
@@ -90,7 +127,12 @@ Rota.add_shifts(
|
||||
constraint=[{"name": "max_shifts_per_week", "options": 2}],
|
||||
),
|
||||
SingleShift(
|
||||
sites=("exeter", "exeter twilights and weekends", "exeter no nights"),
|
||||
sites=(
|
||||
"exeter",
|
||||
"exeter twilights and weekends",
|
||||
"exeter no nights",
|
||||
"exeter weekends",
|
||||
),
|
||||
name="weekend_exeter",
|
||||
length=12.5,
|
||||
days=days[5:],
|
||||
@@ -101,18 +143,25 @@ Rota.add_shifts(
|
||||
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
||||
),
|
||||
SingleShift(
|
||||
sites=("truro",),
|
||||
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,
|
||||
# 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",),
|
||||
sites=("torbay", "torbay twilights and weekends"),
|
||||
name="weekend_torbay",
|
||||
length=12.5,
|
||||
days=days[4:],
|
||||
@@ -123,7 +172,12 @@ Rota.add_shifts(
|
||||
# force_as_block_unless_nwd=True
|
||||
),
|
||||
SingleShift(
|
||||
sites=("plymouth", "plymouth twilights and weekends", "plymouth first on weekends only"),
|
||||
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:],
|
||||
@@ -134,7 +188,11 @@ Rota.add_shifts(
|
||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||
),
|
||||
SingleShift(
|
||||
sites=("plymouth", "plymouth twilights and weekends"),
|
||||
sites=(
|
||||
"plymouth",
|
||||
"plymouth twilights and weekends",
|
||||
"plymouth twilights, weekends and weekend nights",
|
||||
),
|
||||
name="weekend_plymouth2",
|
||||
length=8,
|
||||
days=days[5:],
|
||||
@@ -145,7 +203,12 @@ Rota.add_shifts(
|
||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||
),
|
||||
SingleShift(
|
||||
sites=("plymouth", "plymouth twilights", "plymouth twilights and weekends"),
|
||||
sites=(
|
||||
"plymouth",
|
||||
"plymouth twilights",
|
||||
"plymouth twilights and weekends",
|
||||
"plymouth twilights, weekends and weekend nights",
|
||||
),
|
||||
name="plymouth_bank_holidays",
|
||||
length=8,
|
||||
days=days[5:],
|
||||
@@ -163,38 +226,106 @@ Rota.add_shifts(
|
||||
balance_offset=3.9,
|
||||
balance_weighting=1,
|
||||
# hard_constrain_shift=False,
|
||||
workers_required=3,
|
||||
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": "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),
|
||||
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=3,
|
||||
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": "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_grade_constraint_by_week([2], [1, 2], ["night_weekday", "night_weekend"])
|
||||
@@ -243,8 +374,10 @@ if load_leave:
|
||||
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()
|
||||
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(
|
||||
@@ -259,7 +392,26 @@ if load_leave:
|
||||
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(
|
||||
@@ -270,6 +422,11 @@ if load_leave:
|
||||
).date(),
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
print("WORKER", worker)
|
||||
print("DATES", s, e)
|
||||
raise
|
||||
|
||||
oop = formatted_oops
|
||||
else:
|
||||
@@ -307,6 +464,20 @@ if load_leave:
|
||||
work_requests = w["requests"]
|
||||
site_pref = w["site_pref"]
|
||||
|
||||
shift_fte_overrides = {}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
w = Worker(
|
||||
name=worker_name,
|
||||
site=site.lower(),
|
||||
@@ -324,20 +495,22 @@ if load_leave:
|
||||
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)
|
||||
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)
|
||||
@@ -360,9 +533,15 @@ Rota.build_and_solve(solver_options, export=True)
|
||||
#
|
||||
# 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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
@@ -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)
|
||||
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=',')
|
||||
|
||||
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:
|
||||
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="*"))
|
||||
|
||||
|
||||
|
||||
@@ -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","",""
|
||||
|
@@ -318,3 +318,7 @@ table.transposed th.bank-holiday {
|
||||
.target-assigned.no-colour {
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.locum-shift {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
+33
-1
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
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>';
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# pytest.ini
|
||||
[pytest]
|
||||
testpaths = test
|
||||
python_files = test_*.py
|
||||
+9
-5
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
+757
-221
File diff suppressed because it is too large
Load Diff
+126
-45
@@ -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) < (
|
||||
@@ -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")
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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"
|
||||
Reference in New Issue
Block a user