start working on shift paring balancing
This commit is contained in:
@@ -3,6 +3,10 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
|
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
|
||||||
|
import typer
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
sites = (
|
sites = (
|
||||||
@@ -15,6 +19,8 @@ sites = (
|
|||||||
"proc",
|
"proc",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NIGHT_REGISTRAR_NUMBER = 4
|
||||||
|
|
||||||
from rota.workers import (
|
from rota.workers import (
|
||||||
Worker,
|
Worker,
|
||||||
NotAvailableToWork,
|
NotAvailableToWork,
|
||||||
@@ -24,25 +30,21 @@ from rota.workers import (
|
|||||||
OutOfProgramme,
|
OutOfProgramme,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def main(
|
||||||
|
suspend_on_finish: 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,
|
||||||
|
):
|
||||||
|
|
||||||
suspend_on_finish = False
|
rota_start_date = start_date.date()
|
||||||
solve = True
|
|
||||||
# time_to_run = 508500
|
|
||||||
# time_to_run = 2660000
|
|
||||||
time_to_run = 60 * 60 * 100
|
|
||||||
#time_to_run = 60 * 300
|
|
||||||
# allow = 5000
|
|
||||||
ratio = 0.5
|
|
||||||
#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)
|
|
||||||
|
|
||||||
Rota = RotaBuilder(
|
Rota = RotaBuilder(
|
||||||
start_date, weeks_to_rota=8, 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 = RotaBuilder(start_date, weeks_to_rota=20, balance_offset_modifier=1)
|
||||||
Rota.constraint_options["balance_weekends"] = True
|
Rota.constraint_options["balance_weekends"] = True
|
||||||
@@ -54,7 +56,12 @@ Rota.constraint_options["hard_constrain_pair_separation"] = True
|
|||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("exeter", "exeter twilights and weekends", "exeter no nights"),
|
sites=(
|
||||||
|
"exeter",
|
||||||
|
"exeter twilights and weekends",
|
||||||
|
"exeter no nights",
|
||||||
|
"exeter twilights",
|
||||||
|
),
|
||||||
name="exeter_twilight",
|
name="exeter_twilight",
|
||||||
length=12.5,
|
length=12.5,
|
||||||
days=days[:5],
|
days=days[:5],
|
||||||
@@ -67,7 +74,14 @@ Rota.add_shifts(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("truro", "truro twilights", "truro twilights and weekends"),
|
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",
|
name="truro_twilight",
|
||||||
length=12.5,
|
length=12.5,
|
||||||
days=days[:4],
|
days=days[:4],
|
||||||
@@ -82,7 +96,12 @@ Rota.add_shifts(
|
|||||||
assign_as_block=True,
|
assign_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth", "plymouth twilights", "plymouth twilights and weekends", "plymouth twilights, weekends and weekend nights"),
|
sites=(
|
||||||
|
"plymouth",
|
||||||
|
"plymouth twilights",
|
||||||
|
"plymouth twilights and weekends",
|
||||||
|
"plymouth twilights, weekends and weekend nights",
|
||||||
|
),
|
||||||
name="plymouth_twilight",
|
name="plymouth_twilight",
|
||||||
length=12.5,
|
length=12.5,
|
||||||
days=days[:5],
|
days=days[:5],
|
||||||
@@ -91,7 +110,12 @@ Rota.add_shifts(
|
|||||||
constraint=[{"name": "max_shifts_per_week", "options": 2}],
|
constraint=[{"name": "max_shifts_per_week", "options": 2}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("exeter", "exeter twilights and weekends", "exeter no nights"),
|
sites=(
|
||||||
|
"exeter",
|
||||||
|
"exeter twilights and weekends",
|
||||||
|
"exeter no nights",
|
||||||
|
"exeter weekends",
|
||||||
|
),
|
||||||
name="weekend_exeter",
|
name="weekend_exeter",
|
||||||
length=12.5,
|
length=12.5,
|
||||||
days=days[5:],
|
days=days[5:],
|
||||||
@@ -102,13 +126,20 @@ Rota.add_shifts(
|
|||||||
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("truro", "truro twilights and weekends"),
|
sites=(
|
||||||
|
"truro",
|
||||||
|
"truro no nights",
|
||||||
|
"truro twilights and weekends",
|
||||||
|
"truro weekends",
|
||||||
|
"truro twilights, weekends and weekend nights",
|
||||||
|
),
|
||||||
name="weekend_truro",
|
name="weekend_truro",
|
||||||
length=12.5,
|
length=12.5,
|
||||||
days=days[4:],
|
days=days[4:],
|
||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
rota_on_nwds=True,
|
#rota_on_nwds=True,
|
||||||
force_as_block=True,
|
#force_as_block=True,
|
||||||
|
assign_as_block=True,
|
||||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||||
# force_as_block_unless_nwd=True
|
# force_as_block_unless_nwd=True
|
||||||
),
|
),
|
||||||
@@ -124,7 +155,12 @@ Rota.add_shifts(
|
|||||||
# force_as_block_unless_nwd=True
|
# force_as_block_unless_nwd=True
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth", "plymouth twilights and weekends", "plymouth first on weekends only", "plymouth twilights, weekends and weekend nights"),
|
sites=(
|
||||||
|
"plymouth",
|
||||||
|
"plymouth twilights and weekends",
|
||||||
|
"plymouth first on weekends only",
|
||||||
|
"plymouth twilights, weekends and weekend nights",
|
||||||
|
),
|
||||||
name="weekend_plymouth1",
|
name="weekend_plymouth1",
|
||||||
length=8,
|
length=8,
|
||||||
days=days[5:],
|
days=days[5:],
|
||||||
@@ -135,7 +171,11 @@ Rota.add_shifts(
|
|||||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth", "plymouth twilights and weekends", "plymouth twilights, weekends and weekend nights"),
|
sites=(
|
||||||
|
"plymouth",
|
||||||
|
"plymouth twilights and weekends",
|
||||||
|
"plymouth twilights, weekends and weekend nights",
|
||||||
|
),
|
||||||
name="weekend_plymouth2",
|
name="weekend_plymouth2",
|
||||||
length=8,
|
length=8,
|
||||||
days=days[5:],
|
days=days[5:],
|
||||||
@@ -146,7 +186,12 @@ Rota.add_shifts(
|
|||||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth", "plymouth twilights", "plymouth twilights and weekends", "plymouth twilights, weekends and weekend nights"),
|
sites=(
|
||||||
|
"plymouth",
|
||||||
|
"plymouth twilights",
|
||||||
|
"plymouth twilights and weekends",
|
||||||
|
"plymouth twilights, weekends and weekend nights",
|
||||||
|
),
|
||||||
name="plymouth_bank_holidays",
|
name="plymouth_bank_holidays",
|
||||||
length=8,
|
length=8,
|
||||||
days=days[5:],
|
days=days[5:],
|
||||||
@@ -171,13 +216,23 @@ Rota.add_shifts(
|
|||||||
{"name": "night"},
|
{"name": "night"},
|
||||||
{"name": "pre", "options": 2},
|
{"name": "pre", "options": 2},
|
||||||
{"name": "post", "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": "limit_grade_number", "options": {2: 1}},
|
||||||
{"name": "minimum_grade_number", "options": (4, 1)},
|
{"name": "minimum_grade_number", "options": (4, 1)},
|
||||||
],
|
],
|
||||||
|
end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=[*sites, "plymouth twilights, weekends and weekend nights"],
|
sites=[
|
||||||
|
*sites,
|
||||||
|
"plymouth twilights, weekends and weekend nights",
|
||||||
|
"truro twilights and weekend nights",
|
||||||
|
"weekend nights",
|
||||||
|
"truro twilights, weekends and weekend nights",
|
||||||
|
],
|
||||||
name="night_weekend",
|
name="night_weekend",
|
||||||
length=12.25,
|
length=12.25,
|
||||||
days=days[4:],
|
days=days[4:],
|
||||||
@@ -191,10 +246,68 @@ Rota.add_shifts(
|
|||||||
{"name": "night"},
|
{"name": "night"},
|
||||||
{"name": "pre", "options": 2},
|
{"name": "pre", "options": 2},
|
||||||
{"name": "post", "options": 3},
|
{"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": "limit_grade_number", "options": {2: 1}},
|
||||||
{"name": "minimum_grade_number", "options": (4, 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(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -244,8 +357,10 @@ if load_leave:
|
|||||||
print("Split nwd", worker_name, i)
|
print("Split nwd", worker_name, i)
|
||||||
a, b = i.split("[")[1][:-1].split("-")
|
a, b = i.split("[")[1][:-1].split("-")
|
||||||
print(f"{a=} {b=}")
|
print(f"{a=} {b=}")
|
||||||
nwd_start_date = datetime.datetime.strptime(a, "%d/%m/%y").date()
|
nwd_start_date = datetime.datetime.strptime(
|
||||||
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%y").date()
|
a, "%d/%m/%Y"
|
||||||
|
).date()
|
||||||
|
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%Y").date()
|
||||||
|
|
||||||
nwds.append(
|
nwds.append(
|
||||||
NonWorkingDays(
|
NonWorkingDays(
|
||||||
@@ -261,7 +376,24 @@ if load_leave:
|
|||||||
formatted_oops = []
|
formatted_oops = []
|
||||||
for dates in oop.split(","):
|
for dates in oop.split(","):
|
||||||
print(dates)
|
print(dates)
|
||||||
|
if "-" in dates:
|
||||||
s, e = dates.split("-")
|
s, e = dates.split("-")
|
||||||
|
elif "to" in dates:
|
||||||
|
s, e = dates.split(" to ")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Cannot parse OOP dates: {dates}")
|
||||||
|
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:
|
try:
|
||||||
formatted_oops.append(
|
formatted_oops.append(
|
||||||
{
|
{
|
||||||
@@ -279,7 +411,6 @@ if load_leave:
|
|||||||
print("DATES", s, e)
|
print("DATES", s, e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
oop = formatted_oops
|
oop = formatted_oops
|
||||||
else:
|
else:
|
||||||
oop = []
|
oop = []
|
||||||
@@ -339,14 +470,14 @@ if load_leave:
|
|||||||
|
|
||||||
Rota.add_worker(w)
|
Rota.add_worker(w)
|
||||||
|
|
||||||
|
|
||||||
# Rota.build_workers()
|
# Rota.build_workers()
|
||||||
# Rota.build_model()
|
# Rota.build_model()
|
||||||
|
|
||||||
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
|
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
|
||||||
|
#solver_options = {"seconds": time_to_run, "threads": 10}
|
||||||
|
|
||||||
# start_time = time.time()
|
# start_time = time.time()
|
||||||
Rota.build_and_solve(solver_options, export=True, solve=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_by_block(solver_options, block_length=13)
|
||||||
#Rota.solve_shifts_individually(solver_options)
|
#Rota.solve_shifts_individually(solver_options)
|
||||||
@@ -370,9 +501,13 @@ Rota.build_and_solve(solver_options, export=True, solve=True)
|
|||||||
#
|
#
|
||||||
# print(worker_timetable_brief)
|
# print(worker_timetable_brief)
|
||||||
|
|
||||||
|
|
||||||
# Rota.export_rota_to_html("proc_rota")
|
# Rota.export_rota_to_html("proc_rota")
|
||||||
# Rota.export_rota_to_csv("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:
|
if suspend_on_finish:
|
||||||
os.system("systemctl suspend")
|
os.system("systemctl suspend")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import csv
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import re
|
import re
|
||||||
from requests import Session
|
from requests import Session
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from rich.pretty import pprint
|
||||||
|
|
||||||
|
|
||||||
from rota.workers import Worker, NotAvailableToWork, NonWorkingDays, WorkRequests, PreferenceNotToWork, OutOfProgramme
|
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}"
|
date_re = r"[\d]{1,2}\/[\d]{1,2}\/[\d]{2}"
|
||||||
|
|
||||||
|
|
||||||
|
live_rota = True
|
||||||
|
|
||||||
def load_leave(Rota):
|
def load_leave(Rota):
|
||||||
|
|
||||||
with Session() as s:
|
with Session() as s:
|
||||||
@@ -21,14 +26,17 @@ def load_leave(Rota):
|
|||||||
"Pragma": "no-cache"
|
"Pragma": "no-cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vRYqSKq-OYW1tNnGJtx9LIMo_FXVfcHBRPF4HEMcBxWPfmXimWtr5oEDR_rIUSXrOndb9YjP3iC_4bS/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')
|
decoded_content = download.content.decode('utf-8')
|
||||||
|
|
||||||
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
|
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
|
||||||
|
|
||||||
#with open("/home/ross/Downloads/Draft PROC Rota March 24 - Sept 24 - Sheet1.csv") as f:
|
else:
|
||||||
# reader = csv.reader(f.read().splitlines(), delimiter=',')
|
with open("/home/ross/Downloads/PROC Draft RK version.csv") as f:
|
||||||
|
reader = csv.reader(f.read().splitlines(), delimiter=',')
|
||||||
|
|
||||||
|
n = 0
|
||||||
leave = {}
|
leave = {}
|
||||||
requests = {}
|
requests = {}
|
||||||
site_prefs = {}
|
site_prefs = {}
|
||||||
@@ -37,7 +45,11 @@ def load_leave(Rota):
|
|||||||
|
|
||||||
n = 1
|
n = 1
|
||||||
for row in reader:
|
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:]
|
r = row[2:]
|
||||||
# header, extract names
|
# header, extract names
|
||||||
if n == 1:
|
if n == 1:
|
||||||
@@ -70,16 +82,16 @@ def load_leave(Rota):
|
|||||||
if lower_item == "derriford twilights":
|
if lower_item == "derriford twilights":
|
||||||
lower_item = "plymouth_twilights"
|
lower_item = "plymouth_twilights"
|
||||||
|
|
||||||
if "PROC site" in row_title:
|
if "PROC site" in row_title or row_title == "PROC nights from":
|
||||||
worker["site_pref"] = lower_item
|
worker["site_pref"] = lower_item
|
||||||
|
|
||||||
if row_title in ("Rotation", "Site"):
|
elif row_title in ("Rotation", "Site", "Placement", "Placement location"):
|
||||||
worker["site"] = lower_item
|
worker["site"] = lower_item
|
||||||
|
|
||||||
elif row_title == "Grade":
|
elif row_title in ("Grade", "Grade (ST)"):
|
||||||
worker["grade"] = lower_item
|
worker["grade"] = lower_item
|
||||||
|
|
||||||
elif row_title == "%FTE":
|
elif row_title in ("%FTE", "%FTE on-call", "FTE", "FTE on-call"):
|
||||||
worker["fte"] = lower_item
|
worker["fte"] = lower_item
|
||||||
|
|
||||||
elif "NWD" in row_title:
|
elif "NWD" in row_title:
|
||||||
@@ -98,7 +110,13 @@ def load_leave(Rota):
|
|||||||
|
|
||||||
elif row_title in ("Start date",):
|
elif row_title in ("Start date",):
|
||||||
if lower_item:
|
if lower_item:
|
||||||
|
try:
|
||||||
date = datetime.strptime(lower_item, "%d/%m/%y").date()
|
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
|
worker["start_date"] = date
|
||||||
else:
|
else:
|
||||||
worker["start_date"] = None
|
worker["start_date"] = None
|
||||||
@@ -128,19 +146,25 @@ def load_leave(Rota):
|
|||||||
if lower_item:
|
if lower_item:
|
||||||
worker["bank_holiday_extra"] = int(lower_item)
|
worker["bank_holiday_extra"] = int(lower_item)
|
||||||
|
|
||||||
elif re.match(date_re, row_title) is not None:
|
elif re.match(date_re, row_date) is not None:
|
||||||
|
|
||||||
if lower_item != "":
|
if lower_item != "":
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
date = datetime.strptime(row[0], "%d/%m/%y").date()
|
date = datetime.strptime(row_date, "%d/%m/%y").date()
|
||||||
except ValueError:
|
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
|
# This may be easier to do as a dict
|
||||||
if lower_item in shifts:
|
if lower_item in shifts:
|
||||||
worker["requests"].append(WorkRequests(date=date, shift=lower_item))
|
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="*"))
|
worker["requests"].append(WorkRequests(date=date, shift="*"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ function generateExtra() {
|
|||||||
|
|
||||||
bank_holidays_worked = bank_holidays.length;
|
bank_holidays_worked = bank_holidays.length;
|
||||||
|
|
||||||
|
nights_worked= $(tr).find(".night-shift").length;
|
||||||
|
|
||||||
shift_diff = JSON.parse(worker_td.attr("data-shift-diff"));
|
shift_diff = JSON.parse(worker_td.attr("data-shift-diff"));
|
||||||
summed_shift_diff = Object.values(shift_diff).reduce((a, b) => a + b);
|
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'>
|
worker_td.after(`<div class='worker-summary auto-generated'>
|
||||||
<span>Total shifts: ${total_shifts}, </span>
|
<span>Total shifts: ${total_shifts}, </span>
|
||||||
<span>Weekends: ${weekends_worked}, </span>
|
<span>Weekends: ${weekends_worked}, </span>
|
||||||
|
<span>Nights: ${nights_worked}, </span>
|
||||||
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
||||||
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
|
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
|
||||||
|
|
||||||
|
|||||||
+51
-15
@@ -125,6 +125,7 @@ class SingleShift(BaseModel):
|
|||||||
constraint: List[ShiftConstraint] = []
|
constraint: List[ShiftConstraint] = []
|
||||||
start_date: datetime.date | None = None
|
start_date: datetime.date | None = None
|
||||||
end_date: datetime.date | None = None
|
end_date: datetime.date | None = None
|
||||||
|
pair_proxy: bool = False
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
extra = "allow",
|
extra = "allow",
|
||||||
@@ -280,6 +281,8 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
self.paired_shifts = []
|
self.paired_shifts = []
|
||||||
|
|
||||||
|
self.exported_rota_file = None
|
||||||
|
|
||||||
def set_rota_dates(self, start_date: datetime.date, weeks_to_rota: int):
|
def set_rota_dates(self, start_date: datetime.date, weeks_to_rota: int):
|
||||||
self.weeks_to_rota = weeks_to_rota
|
self.weeks_to_rota = weeks_to_rota
|
||||||
|
|
||||||
@@ -1118,13 +1121,14 @@ class RotaBuilder(object):
|
|||||||
print(shift_name)
|
print(shift_name)
|
||||||
print(week, self.get_week_start_date(week), shift.start_date, shift.end_date)
|
print(week, self.get_week_start_date(week), shift.start_date, shift.end_date)
|
||||||
|
|
||||||
if self.get_week_start_date(week) <= shift.start_date:
|
#if self.get_week_start_date(week) <= shift.start_date:
|
||||||
continue
|
# continue
|
||||||
|
|
||||||
if self.get_week_start_date(week) > shift.end_date:
|
#if self.get_week_start_date(week) > shift.end_date:
|
||||||
continue
|
# continue
|
||||||
|
|
||||||
for worker in self.get_workers_for_shift(shift):
|
for worker in self.get_workers_for_shift(shift):
|
||||||
|
try:
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
8
|
8
|
||||||
* self.model.blocks_worker_shift_assigned[
|
* self.model.blocks_worker_shift_assigned[
|
||||||
@@ -1135,6 +1139,8 @@ class RotaBuilder(object):
|
|||||||
for day in shift.days
|
for day in shift.days
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
self.model.blocks_assigned[week, shift_name]
|
self.model.blocks_assigned[week, shift_name]
|
||||||
== sum(
|
== sum(
|
||||||
@@ -2161,15 +2167,23 @@ class RotaBuilder(object):
|
|||||||
# shift spans 7 days you may get >7 allocations in a row
|
# shift spans 7 days you may get >7 allocations in a row
|
||||||
# as it only checks for a different shift allocation
|
# as it only checks for a different shift allocation
|
||||||
for constraint_shift in self.get_shifts_with_constraints(
|
for constraint_shift in self.get_shifts_with_constraints(
|
||||||
"pre", week=week
|
"pre"#, week=week
|
||||||
):
|
):
|
||||||
for n in range(0, constraint_shift.constraint_options["pre"]):
|
for n in range(0, constraint_shift.constraint_options["pre"]):
|
||||||
if day in constraint_shift.days:
|
if day in constraint_shift.days:
|
||||||
self.model.constraints.add(
|
#week_start_date = self.get_week_start_date(week)
|
||||||
1
|
#try:
|
||||||
>= self.model.works[
|
#if constraint_shift.start_date <= week_start_date < constraint_shift.end_date:
|
||||||
|
# works = 1
|
||||||
|
try:
|
||||||
|
works = self.model.works[
|
||||||
worker.id, week, day, constraint_shift.name
|
worker.id, week, day, constraint_shift.name
|
||||||
]
|
]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
self.model.constraints.add(
|
||||||
|
1
|
||||||
|
>= works
|
||||||
+ sum(
|
+ sum(
|
||||||
pre_map[n][0]
|
pre_map[n][0]
|
||||||
* self.model.works[
|
* self.model.works[
|
||||||
@@ -2209,15 +2223,25 @@ class RotaBuilder(object):
|
|||||||
# )
|
# )
|
||||||
|
|
||||||
for constraint_shift in self.get_shifts_with_constraints(
|
for constraint_shift in self.get_shifts_with_constraints(
|
||||||
"post", week=week
|
"post"#, week=week
|
||||||
):
|
):
|
||||||
for n in range(0, constraint_shift.constraint_options["post"]):
|
for n in range(0, constraint_shift.constraint_options["post"]):
|
||||||
if day in constraint_shift.days:
|
if day in constraint_shift.days:
|
||||||
self.model.constraints.add(
|
#week_start_date = self.get_week_start_date(week)
|
||||||
1
|
#if constraint_shift.start_date <= week_start_date < constraint_shift.end_date:
|
||||||
>= self.model.works[
|
#works = 1
|
||||||
|
#else:
|
||||||
|
try:
|
||||||
|
works = self.model.works[
|
||||||
worker.id, week, day, constraint_shift.name
|
worker.id, week, day, constraint_shift.name
|
||||||
]
|
]
|
||||||
|
except KeyError:
|
||||||
|
# If we can't find the key the shift is not assigned
|
||||||
|
# so we don't need to worry about it
|
||||||
|
continue
|
||||||
|
self.model.constraints.add(
|
||||||
|
1
|
||||||
|
>= works
|
||||||
+ sum(
|
+ sum(
|
||||||
post_map[n][0]
|
post_map[n][0]
|
||||||
* self.model.works[
|
* self.model.works[
|
||||||
@@ -2230,6 +2254,8 @@ class RotaBuilder(object):
|
|||||||
for w in workers
|
for w in workers
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
#except KeyError:
|
||||||
|
# pass
|
||||||
|
|
||||||
# for constraint_shift in self.get_shifts_with_constraints(
|
# for constraint_shift in self.get_shifts_with_constraints(
|
||||||
# "preclear", "preclear2"
|
# "preclear", "preclear2"
|
||||||
@@ -2664,7 +2690,7 @@ class RotaBuilder(object):
|
|||||||
def pair_shifts(self, shift1: SingleShift, shift2: SingleShift):
|
def pair_shifts(self, shift1: SingleShift, shift2: SingleShift):
|
||||||
# NOTE: currently designed to pair two shifts
|
# NOTE: currently designed to pair two shifts
|
||||||
# this could be extended...
|
# this could be extended...
|
||||||
self.pair_shifts.append(set(shift1, shift2))
|
self.paired_shifts.append([shift1, shift2])
|
||||||
|
|
||||||
def build_shifts(self):
|
def build_shifts(self):
|
||||||
"""
|
"""
|
||||||
@@ -2684,6 +2710,11 @@ class RotaBuilder(object):
|
|||||||
for s in self.shifts:
|
for s in self.shifts:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
## For paired shifts we create proxy shifts
|
||||||
|
#self.paired_shift_map = {}
|
||||||
|
#for shifts in self.paired_shifts:
|
||||||
|
|
||||||
|
|
||||||
for s in self.shifts:
|
for s in self.shifts:
|
||||||
if s.name in self.shift_names:
|
if s.name in self.shift_names:
|
||||||
raise InvalidShift(f"Duplicate shift: {s.name}")
|
raise InvalidShift(f"Duplicate shift: {s.name}")
|
||||||
@@ -2746,6 +2777,7 @@ class RotaBuilder(object):
|
|||||||
for shift in self.get_shifts_with_constraint("post"):
|
for shift in self.get_shifts_with_constraint("post"):
|
||||||
self.max_post = max(shift.constraint_options["post"], self.max_post)
|
self.max_post = max(shift.constraint_options["post"], self.max_post)
|
||||||
|
|
||||||
|
|
||||||
# # Todo replace with week_day.....
|
# # Todo replace with week_day.....
|
||||||
# self.day_shift_product = []
|
# self.day_shift_product = []
|
||||||
# self.day_shiftclass_product = []
|
# self.day_shiftclass_product = []
|
||||||
@@ -3038,6 +3070,7 @@ class RotaBuilder(object):
|
|||||||
f.write(self.get_worker_timetable_html(True))
|
f.write(self.get_worker_timetable_html(True))
|
||||||
url = urllib.parse.quote(str(output_file.resolve()))
|
url = urllib.parse.quote(str(output_file.resolve()))
|
||||||
print(f"Rota exported [bold blue]('[link url={url}]{url}')[/bold blue]")
|
print(f"Rota exported [bold blue]('[link url={url}]{url}')[/bold blue]")
|
||||||
|
self.exported_rota_file = output_file
|
||||||
|
|
||||||
def export_rota_to_csv(self, filename: str = "rota"):
|
def export_rota_to_csv(self, filename: str = "rota"):
|
||||||
output_file = Path("output", f"{filename}.csv")
|
output_file = Path("output", f"{filename}.csv")
|
||||||
@@ -3272,6 +3305,8 @@ class RotaBuilder(object):
|
|||||||
shift = self.get_shift_by_name(shift_name)
|
shift = self.get_shift_by_name(shift_name)
|
||||||
if "require_remote_site_presence_week" in shift.constraints:
|
if "require_remote_site_presence_week" in shift.constraints:
|
||||||
remote_site = f" data-shift-remote-site='{shift.constraint_options['require_remote_site_presence_week'][0]}'"
|
remote_site = f" data-shift-remote-site='{shift.constraint_options['require_remote_site_presence_week'][0]}'"
|
||||||
|
if "night" in shift.constraints:
|
||||||
|
css_class = " ".join((css_class, "night-shift"))
|
||||||
|
|
||||||
shift_tds.append(
|
shift_tds.append(
|
||||||
f"<td title='{title}' class='rota-day {css_class}' data-shift='{shift_name}' data-available='{available}' data-unavailable_reason='{unavailable_reason}' data-date='{d}' data-week='{week}' data-day='{day}'{remote_site}{requests}{bank_holiday}>{a}</td>"
|
f"<td title='{title}' class='rota-day {css_class}' data-shift='{shift_name}' data-available='{available}' data-unavailable_reason='{unavailable_reason}' data-date='{d}' data-week='{week}' data-day='{day}'{remote_site}{requests}{bank_holiday}>{a}</td>"
|
||||||
@@ -3453,7 +3488,7 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
return timetable
|
return timetable
|
||||||
|
|
||||||
def get_worker_shifts_by_date(self, worker: Worker) -> Dict:
|
def get_worker_shifts_by_date(self, worker: Worker) -> Dict[datetime.date, str]:
|
||||||
shifts = {}
|
shifts = {}
|
||||||
|
|
||||||
n = 0
|
n = 0
|
||||||
@@ -3472,6 +3507,7 @@ class RotaBuilder(object):
|
|||||||
shifts = {}
|
shifts = {}
|
||||||
|
|
||||||
for worker in self.workers:
|
for worker in self.workers:
|
||||||
|
print("*", worker.name)
|
||||||
shifts[worker.name] = len(
|
shifts[worker.name] = len(
|
||||||
[i for i in self.get_worker_shift_list(worker) if i != ""]
|
[i for i in self.get_worker_shift_list(worker) if i != ""]
|
||||||
)
|
)
|
||||||
@@ -3487,7 +3523,7 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
temp = ""
|
temp = ""
|
||||||
for shift in self.get_shift_names_by_week_day(week, day):
|
for shift in self.get_shift_names_by_week_day(week, day):
|
||||||
if self.model.works[worker.id, week, day, shift].value > 0:
|
if self.model.works[worker.id, week, day, shift].value > 0.90:
|
||||||
temp = shift
|
temp = shift
|
||||||
shifts.append(temp)
|
shifts.append(temp)
|
||||||
|
|
||||||
|
|||||||
@@ -598,7 +598,11 @@ class TestDemoRotaBalanceShiftSites:
|
|||||||
self.Rota.constraint_options["balance_nights"] = False
|
self.Rota.constraint_options["balance_nights"] = False
|
||||||
self.Rota.constraint_options["constrain_time_off_after_nights"] = False
|
self.Rota.constraint_options["constrain_time_off_after_nights"] = False
|
||||||
|
|
||||||
|
try:
|
||||||
self.Rota.terminate_on_warning.remove("Worker/no valid shifts")
|
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})
|
self.Rota.build_and_solve(options={"ratio": 0.1})
|
||||||
|
|
||||||
group_workers = self.Rota.get_workers_by_group()
|
group_workers = self.Rota.get_workers_by_group()
|
||||||
|
|||||||
+162
-1
@@ -2,7 +2,7 @@ import datetime
|
|||||||
import pytest
|
import pytest
|
||||||
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, WarningTermination, days
|
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, WarningTermination, days
|
||||||
|
|
||||||
from rota.workers import Worker
|
from rota.workers import NotAvailableToWork, WorkRequests, Worker
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
@@ -439,3 +439,164 @@ class TestShiftDates:
|
|||||||
for worker in Rota.get_workers():
|
for worker in Rota.get_workers():
|
||||||
assert Rota.get_worker_shift_list_string(worker).startswith("-"*7)
|
assert Rota.get_worker_shift_list_string(worker).startswith("-"*7)
|
||||||
assert Rota.get_worker_shift_list_string(worker).endswith("-"*28)
|
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"
|
||||||
|
|
||||||
|
def test_shift_start_date_end_date_block_paired(self):
|
||||||
|
Rota = generate_basic_rota(workers=7)
|
||||||
|
|
||||||
|
|
||||||
|
request_date = Rota.start_date + datetime.timedelta(days=(7*6)+4)
|
||||||
|
|
||||||
|
no_work = [NotAvailableToWork(date=d, shift="*") for d in date_generator(request_date, 18)]
|
||||||
|
|
||||||
|
Rota.add_workers(
|
||||||
|
[
|
||||||
|
Worker(name=f"worker-wr-{i}", site="group2", grade=1, not_available_to_work=no_work) for i in range(1, 4)
|
||||||
|
#Worker(name="worker2", site="group1", grade=1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
a, b, c = (SingleShift(
|
||||||
|
sites=("group1", "group2"), name="a", length= 12.5, days=days[:4],
|
||||||
|
workers_required=8,
|
||||||
|
constraint=[{"name": "max_shifts_per_week", "options": 4}],
|
||||||
|
end_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.add_shifts(
|
||||||
|
a,
|
||||||
|
b, c
|
||||||
|
)
|
||||||
|
Rota.pair_shifts(b, c)
|
||||||
|
|
||||||
|
Rota.build_and_solve(options={"ratio": 0.01})
|
||||||
|
Rota.export_rota_to_html("test_shift_start_date")
|
||||||
|
|
||||||
|
assert Rota.results.solver.status == "ok"
|
||||||
|
assert Rota.results.solver.termination_condition == "optimal"
|
||||||
Reference in New Issue
Block a user