Compare commits
3
Commits
23529ba441
...
62e48ef205
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e48ef205 | ||
|
|
b98c28401f | ||
|
|
94b921192f |
+6
-2
@@ -4,7 +4,7 @@ import datetime
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from rota_generator.shifts import MaxShiftsPerWeekConstraint, NoWorkers, PostShiftConstraint, PreShiftConstraint, RotaBuilder, SingleShift, WorkerRequirement, days, RotaConstraintOptions
|
||||
from rota_generator.shifts import MaxShiftsPerWeekConstraint, NoWorkers, PostShiftConstraint, PreShiftConstraint, RotaBuilder, SingleShift, WorkerRequirement, days, RotaConstraintOptions, MaxShiftsPerWeekBlockConstraint
|
||||
import typer
|
||||
import subprocess
|
||||
import pandas as pd
|
||||
@@ -404,6 +404,9 @@ def load_workers():
|
||||
previous_shifts=priors_map.get(initial, {}),
|
||||
)
|
||||
|
||||
if initial == "LB":
|
||||
w.start_date = datetime.date(2026, 6, 15)
|
||||
|
||||
#if initial == "L1":
|
||||
# w.oop = [OutOfProgramme(
|
||||
# start_date="2025-09-15",
|
||||
@@ -482,7 +485,7 @@ def main(
|
||||
weeks_to_rota=weeks,
|
||||
balance_offset_modifier=bom,
|
||||
use_previous_shifts=True,
|
||||
name="cons rota feb 2026 run 2",
|
||||
name="cons rota feb 2026 run 5",
|
||||
allow_force_assignment_with_leave_conflict=True,
|
||||
constraint_options=RotaConstraintOptions(
|
||||
balance_weekends=True,
|
||||
@@ -551,6 +554,7 @@ def main(
|
||||
constraints=[
|
||||
PreShiftConstraint(days=1, start_date="2025-11-17", ignore_shifts=["oncall"], exclude_days=("Sat", "Sun")),
|
||||
MaxShiftsPerWeekConstraint(max_shifts=1),
|
||||
MaxShiftsPerWeekBlockConstraint(week_block=3,max_shifts=1)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
+76
-75
@@ -2,7 +2,20 @@ import datetime
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from rota_generator.shifts import NoWorkers, RotaBuilder, SingleShift, WorkerRequirement, days
|
||||
from rota_generator.shifts import (
|
||||
NoWorkers,
|
||||
RotaBuilder,
|
||||
SingleShift,
|
||||
WorkerRequirement,
|
||||
days,
|
||||
PreShiftConstraint,
|
||||
PostShiftConstraint,
|
||||
NightConstraint,
|
||||
RequireRemoteSitePresenceConstraint,
|
||||
LimitGradeNumberConstraint,
|
||||
MinimumGradeNumberConstraint,
|
||||
MaxShiftsPerWeekConstraint,
|
||||
)
|
||||
import typer
|
||||
import subprocess
|
||||
|
||||
@@ -35,8 +48,8 @@ def main(
|
||||
solve: bool = True,
|
||||
time_to_run: int = 60 * 60 * 4,
|
||||
ratio: float = 0.001,
|
||||
start_date: datetime.datetime = "2025-09-01",
|
||||
weeks: int = 16,
|
||||
start_date: datetime.datetime = "2026-03-02",
|
||||
weeks: int = 12,
|
||||
bom: int = 2,
|
||||
):
|
||||
rota_start_date = start_date.date()
|
||||
@@ -81,12 +94,7 @@ def main(
|
||||
length=12.5,
|
||||
days=days[:5],
|
||||
balance_offset=4,
|
||||
constraint=[
|
||||
{
|
||||
"name": "max_shifts_per_week",
|
||||
"options": 2,
|
||||
}
|
||||
],
|
||||
constraints=[MaxShiftsPerWeekConstraint(max_shifts=2)],
|
||||
),
|
||||
SingleShift(
|
||||
sites=(
|
||||
@@ -122,7 +130,7 @@ def main(
|
||||
days=days[:5],
|
||||
balance_offset=4,
|
||||
workers_required=1,
|
||||
constraint=[{"name": "max_shifts_per_week", "options": 2}],
|
||||
constraints=[MaxShiftsPerWeekConstraint(max_shifts=2)],
|
||||
),
|
||||
SingleShift(
|
||||
sites=(
|
||||
@@ -130,6 +138,7 @@ def main(
|
||||
"exeter twilights and weekends",
|
||||
"exeter no nights",
|
||||
"exeter weekends",
|
||||
"exeter twilights",
|
||||
),
|
||||
name="weekend_exeter",
|
||||
length=12.5,
|
||||
@@ -137,7 +146,7 @@ def main(
|
||||
balance_offset=3,
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True,
|
||||
constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 2}],
|
||||
constraints=[PostShiftConstraint(days=2), PreShiftConstraint(days=2)],
|
||||
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
||||
),
|
||||
SingleShift(
|
||||
@@ -155,7 +164,7 @@ def main(
|
||||
# rota_on_nwds=True,
|
||||
# force_as_block=True,
|
||||
assign_as_block=True,
|
||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||
# force_as_block_unless_nwd=True
|
||||
),
|
||||
SingleShift(
|
||||
@@ -166,7 +175,7 @@ def main(
|
||||
balance_offset=3,
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True,
|
||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||
# force_as_block_unless_nwd=True
|
||||
),
|
||||
SingleShift(
|
||||
@@ -183,7 +192,7 @@ def main(
|
||||
workers_required=1,
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True,
|
||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||
),
|
||||
SingleShift(
|
||||
sites=(
|
||||
@@ -198,7 +207,7 @@ def main(
|
||||
workers_required=1,
|
||||
rota_on_nwds=True,
|
||||
force_as_block=True,
|
||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||
),
|
||||
SingleShift(
|
||||
sites=(
|
||||
@@ -227,16 +236,13 @@ def main(
|
||||
workers_required=4,
|
||||
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)},
|
||||
constraints=[
|
||||
NightConstraint(),
|
||||
PreShiftConstraint(days=2),
|
||||
PostShiftConstraint(days=2),
|
||||
RequireRemoteSitePresenceConstraint(site="plymouth", required_number=1),
|
||||
LimitGradeNumberConstraint(grade=2, max_number=1),
|
||||
MinimumGradeNumberConstraint(grade=4, min_number=1),
|
||||
],
|
||||
#end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
|
||||
),
|
||||
@@ -257,16 +263,13 @@ def main(
|
||||
workers_required=4,
|
||||
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: 2}},
|
||||
{"name": "minimum_grade_number", "options": (4, 1)},
|
||||
constraints=[
|
||||
NightConstraint(),
|
||||
PreShiftConstraint(days=2),
|
||||
PostShiftConstraint(days=3),
|
||||
RequireRemoteSitePresenceConstraint(site="plymouth", required_number=1),
|
||||
LimitGradeNumberConstraint(grade=2, max_number=2),
|
||||
MinimumGradeNumberConstraint(grade=4, min_number=1),
|
||||
],
|
||||
#end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
|
||||
),
|
||||
@@ -301,13 +304,18 @@ def main(
|
||||
site = w["site"]
|
||||
except KeyError:
|
||||
print(f"Worker {worker} has no site")
|
||||
raise KeyError
|
||||
print(w)
|
||||
sys.exit(1)
|
||||
grade = w["grade"]
|
||||
try:
|
||||
fte = float(w["fte"]) * 100
|
||||
except ValueError:
|
||||
print(f"{worker} has invalid fte: {fte}")
|
||||
raise ValueError
|
||||
except KeyError:
|
||||
print(f"Worker {worker} has no FTE")
|
||||
print(w)
|
||||
sys.exit(1)
|
||||
|
||||
nwd = w["nwd"]
|
||||
end_date = w["end_date"]
|
||||
@@ -349,41 +357,36 @@ def main(
|
||||
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 ")
|
||||
raw = dates.strip()
|
||||
# strip surrounding brackets/parentheses/quotes
|
||||
raw = raw.strip("()[]\"' ")
|
||||
if "-" in raw:
|
||||
s, e = raw.split("-", 1)
|
||||
elif " to " in raw:
|
||||
s, e = raw.split(" to ", 1)
|
||||
elif "to" in raw:
|
||||
s, e = raw.split("to", 1)
|
||||
else:
|
||||
raise ValueError(f"Cannot parse OOP dates: '{oop}' for {worker}")
|
||||
|
||||
s = s.strip().strip("()[]\"' ")
|
||||
e = e.strip().strip("()[]\"' ")
|
||||
|
||||
parsed = False
|
||||
for fmt in ("%d/%m/%Y", "%d/%m/%y"):
|
||||
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(),
|
||||
}
|
||||
)
|
||||
start_dt = datetime.datetime.strptime(s, fmt).date()
|
||||
end_dt = datetime.datetime.strptime(e, fmt).date()
|
||||
formatted_oops.append({"start_date": start_dt, "end_date": end_dt})
|
||||
parsed = True
|
||||
break
|
||||
except ValueError:
|
||||
try:
|
||||
formatted_oops.append(
|
||||
{
|
||||
"start_date": datetime.datetime.strptime(
|
||||
s.strip(), "%d/%m/%y"
|
||||
).date(),
|
||||
"end_date": datetime.datetime.strptime(
|
||||
e.strip(), "%d/%m/%y"
|
||||
).date(),
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
continue
|
||||
|
||||
if not parsed:
|
||||
print("WORKER", worker)
|
||||
print("DATES", s, e)
|
||||
raise
|
||||
raise ValueError(f"Cannot parse OOP dates: '{oop}' for {worker}")
|
||||
|
||||
oop = formatted_oops
|
||||
else:
|
||||
@@ -423,21 +426,19 @@ def main(
|
||||
|
||||
shift_fte_overrides = {}
|
||||
|
||||
#if worker_name == "Ben Kemp":
|
||||
# shift_fte_overrides = {
|
||||
# "plymouth_twilight": 100,
|
||||
# "weekend_plymouth1": 50,
|
||||
# "weekend_plymouth2": 50,
|
||||
# }
|
||||
if worker_name == "Hadi Mohamed":
|
||||
shift_fte_overrides = {
|
||||
"weekend_exeter": 50,
|
||||
}
|
||||
#elif worker_name == "Joel Lim":
|
||||
# shift_fte_overrides = {
|
||||
# "plymouth_twilight": 100,
|
||||
# "weekend_exeter": 50,
|
||||
# }
|
||||
if worker_name == "Nang Thiriphoo":
|
||||
shift_fte_overrides = {
|
||||
"weekend_exeter": 40,
|
||||
}
|
||||
#if worker_name == "Nang Thiriphoo":
|
||||
# shift_fte_overrides = {
|
||||
# "weekend_exeter": 40,
|
||||
# }
|
||||
|
||||
w = Worker(
|
||||
name=worker_name,
|
||||
@@ -1,41 +1,49 @@
|
||||
import csv
|
||||
from datetime import datetime
|
||||
import re
|
||||
import sys
|
||||
from requests import Session
|
||||
from loguru import logger
|
||||
|
||||
from rich.pretty import pprint
|
||||
|
||||
|
||||
from rota_generator.workers import Worker, NotAvailableToWork, NonWorkingDays, WorkRequests, PreferenceNotToWork, OutOfProgramme
|
||||
from rota_generator.workers import (
|
||||
Worker,
|
||||
NotAvailableToWork,
|
||||
NonWorkingDays,
|
||||
WorkRequests,
|
||||
PreferenceNotToWork,
|
||||
OutOfProgramme,
|
||||
)
|
||||
|
||||
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:
|
||||
shifts = Rota.get_shift_names()
|
||||
|
||||
workers = {}
|
||||
|
||||
headers = {
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache"
|
||||
}
|
||||
headers = {"Cache-Control": "no-cache", "Pragma": "no-cache"}
|
||||
|
||||
if live_rota:
|
||||
download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vSfcQi5Qs__A8hE2CVvUA_6ULNJALrtZOBHPUZ3xyrxPif9obtQF2IqhioT_4nebjHV1Ac5iqhNtuq4/pub?gid=2024511103&single=true&output=csv", headers=headers)
|
||||
#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')
|
||||
download = s.get(
|
||||
"https://docs.google.com/spreadsheets/d/e/2PACX-1vQtNrp9F1fjtAh8vFX-W_R3RlD9H-2P_vCLMoHrVKR0e24yabbsdrD65VjwxoHlV1Qnatmw1NeghQHG/pub?gid=1199196967&single=true&output=csv",
|
||||
headers=headers,
|
||||
)
|
||||
# download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vSRx9VWXSlRubPyA0RhiI-Oqf5eHNYYEc6rFzlraDbR5_8qqr5g13-4uV-gn4u-TjZxiSMv1fBUaESq/pub?gid=814517272&single=true&output=csv", headers=headers)
|
||||
decoded_content = download.content.decode("utf-8")
|
||||
|
||||
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
|
||||
reader = csv.reader(decoded_content.splitlines(), delimiter=",")
|
||||
|
||||
else:
|
||||
with open("/home/ross/Downloads/PROC Draft RK version.csv") as f:
|
||||
reader = csv.reader(f.read().splitlines(), delimiter=',')
|
||||
reader = csv.reader(f.read().splitlines(), delimiter=",")
|
||||
|
||||
n = 0
|
||||
leave = {}
|
||||
@@ -48,7 +56,7 @@ def load_leave(Rota):
|
||||
for row in reader:
|
||||
if n < 10:
|
||||
print(row)
|
||||
row_title = row[1]
|
||||
row_title = row[0]
|
||||
print(row_title)
|
||||
row_date = row[0]
|
||||
r = row[2:]
|
||||
@@ -80,44 +88,81 @@ def load_leave(Rota):
|
||||
|
||||
worker = workers[a]
|
||||
|
||||
lower_item = r[i].lower()
|
||||
lower_item = r[i].lower().strip()
|
||||
if lower_item == "derriford":
|
||||
lower_item = "plymouth"
|
||||
|
||||
if lower_item == "derriford twilights":
|
||||
lower_item = "plymouth_twilights"
|
||||
|
||||
if "PROC site" in row_title or row_title == "PROC nights from" or "proc nights" in row_title.lower():
|
||||
if (
|
||||
"PROC site" in row_title
|
||||
or row_title == "PROC nights from"
|
||||
or "proc nights" in row_title.lower()
|
||||
):
|
||||
worker["site_pref"] = lower_item
|
||||
|
||||
elif row_title in ("Rotation", "Site", "Placement", "Placement location"):
|
||||
elif row_title in (
|
||||
"Rotation",
|
||||
"Site",
|
||||
"Placement",
|
||||
"Placement location",
|
||||
"Placement location",
|
||||
):
|
||||
worker["site"] = lower_item
|
||||
|
||||
elif row_title in ("Grade", "Grade (ST)") or "Year of training" in row_title:
|
||||
elif (
|
||||
row_title in ("Grade", "Grade (ST)")
|
||||
or "Year of training" in row_title
|
||||
):
|
||||
worker["grade"] = lower_item.lstrip("ST")
|
||||
|
||||
elif row_title in ("%FTE", "%FTE on-call", "FTE", "FTE on-call") or "on-call commitment" in row_title:
|
||||
elif (
|
||||
row_title
|
||||
in (
|
||||
"%FTE",
|
||||
"%FTE on-call",
|
||||
"FTE",
|
||||
"FTE on-call",
|
||||
"Full-time equivalent (FTE)",
|
||||
)
|
||||
or "on-call commitment" in row_title
|
||||
):
|
||||
if lower_item:
|
||||
if lower_item.endswith("%"):
|
||||
lower_item = float(lower_item.rstrip("%")) / 100
|
||||
worker["fte"] = lower_item
|
||||
|
||||
elif "NWD" in row_title:
|
||||
elif (
|
||||
"NWD" in row_title or row_title == "If <100% FTE, specify days off:"
|
||||
):
|
||||
worker["nwd"] = lower_item
|
||||
|
||||
elif row_title == "Flexible NWD":
|
||||
worker["flexible_nwd"] = lower_item
|
||||
|
||||
elif row_title in ("End Date", "CCT date"):
|
||||
elif row_title in ("End Date", "CCT date", "CCT 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()
|
||||
try:
|
||||
date = datetime.strptime(lower_item, "%d-%m-%y").date()
|
||||
except ValueError:
|
||||
try:
|
||||
date = datetime.strptime(
|
||||
lower_item, "%d/%m/%Y"
|
||||
).date()
|
||||
except ValueError:
|
||||
print(
|
||||
f"Cannot parse date: '{lower_item}' (n: {n}, row: {row})"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
worker["end_date"] = date
|
||||
else:
|
||||
worker["end_date"] = None
|
||||
|
||||
|
||||
elif row_title in ("Start date",):
|
||||
if lower_item:
|
||||
try:
|
||||
@@ -125,13 +170,22 @@ def load_leave(Rota):
|
||||
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})")
|
||||
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 "OOP" in row_title or "out of programme" in row_title.lower():
|
||||
if lower_item in (
|
||||
"OOPT 1 START",
|
||||
"OOPT 1 END",
|
||||
"OOPT 2 START",
|
||||
"OOPT 2 END",
|
||||
):
|
||||
continue
|
||||
worker["oop"] = lower_item
|
||||
|
||||
elif row_title == "Group":
|
||||
@@ -157,7 +211,6 @@ def load_leave(Rota):
|
||||
worker["bank_holiday_extra"] = int(lower_item)
|
||||
|
||||
elif re.match(date_re, row_date) is not None:
|
||||
|
||||
if lower_item != "":
|
||||
try:
|
||||
try:
|
||||
@@ -167,29 +220,41 @@ def load_leave(Rota):
|
||||
|
||||
# This may be easier to do as a dict
|
||||
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"))
|
||||
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", "may be", "volunteered"):
|
||||
worker["requests"].append(WorkRequests(date=date, shift="*"))
|
||||
worker["requests"].append(
|
||||
WorkRequests(date=date, shift="night_weekend")
|
||||
)
|
||||
|
||||
elif lower_item in (
|
||||
"work_request",
|
||||
"happy to work",
|
||||
"offered",
|
||||
"may be",
|
||||
"volunteered",
|
||||
):
|
||||
worker["requests"].append(
|
||||
WorkRequests(date=date, shift="*")
|
||||
)
|
||||
|
||||
else:
|
||||
worker["leave"].append(NotAvailableToWork(date=date, reason=lower_item))
|
||||
worker["leave"].append(
|
||||
NotAvailableToWork(date=date, reason=lower_item)
|
||||
)
|
||||
except ValueError as e:
|
||||
print(f"Error with: {lower_item}")
|
||||
print(f"{row=}")
|
||||
raise e
|
||||
|
||||
|
||||
n = n + 1
|
||||
|
||||
|
||||
|
||||
return workers
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user