refactor: update default parameters and improve CSV download links in gen_proc and leave modules
This commit is contained in:
+15
-16
@@ -47,9 +47,9 @@ def main(
|
|||||||
suspend: bool = False,
|
suspend: bool = False,
|
||||||
solve: bool = True,
|
solve: bool = True,
|
||||||
time_to_run: int = 60 * 60 * 10,
|
time_to_run: int = 60 * 60 * 10,
|
||||||
ratio: float = 0.0001,
|
ratio: float = 0.1,
|
||||||
start_date: datetime.datetime = "2026-03-02",
|
start_date: datetime.datetime = "2026-09-07",
|
||||||
weeks: int = 27,
|
weeks: int = 26,
|
||||||
bom: float = 1,
|
bom: float = 1,
|
||||||
):
|
):
|
||||||
rota_start_date = start_date.date()
|
rota_start_date = start_date.date()
|
||||||
@@ -59,18 +59,17 @@ def main(
|
|||||||
rota_start_date,
|
rota_start_date,
|
||||||
weeks_to_rota=weeks,
|
weeks_to_rota=weeks,
|
||||||
balance_offset_modifier=bom,
|
balance_offset_modifier=bom,
|
||||||
name="proc_rota3",
|
name="proc_rota_sep_2026",
|
||||||
)
|
)
|
||||||
# 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_shifts"] = False
|
Rota.constraint_options["balance_shifts"] = False
|
||||||
Rota.constraint_options["balance_shifts_quadratic"] = True
|
Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
Rota.constraint_options["balance_weekends"] = True
|
Rota.constraint_options["balance_weekends"] = True
|
||||||
Rota.constraint_options["max_night_frequency"] = 4 # 3
|
#Rota.constraint_options["max_night_frequency"] = 4 # 3
|
||||||
Rota.constraint_options["max_night_frequency_week_exclusions"] = []
|
Rota.constraint_options["max_night_frequency_week_exclusions"] = []
|
||||||
Rota.constraint_options["max_weekend_frequency"] = 3
|
#Rota.constraint_options["max_weekend_frequency"] = 3
|
||||||
Rota.constraint_options["hard_constrain_pair_separation"] = True
|
|
||||||
|
|
||||||
Rota.constraint_options["max_days_per_week_block"] = [(8,4)]
|
#Rota.constraint_options["max_days_per_week_block"] = [(8,4)]
|
||||||
|
|
||||||
#wr = [
|
#wr = [
|
||||||
# WorkerRequirement(
|
# WorkerRequirement(
|
||||||
@@ -376,9 +375,9 @@ def main(
|
|||||||
nwd_end_date = Rota.rota_end_date
|
nwd_end_date = Rota.rota_end_date
|
||||||
|
|
||||||
if "[" in i:
|
if "[" in i:
|
||||||
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(
|
nwd_start_date = datetime.datetime.strptime(
|
||||||
a, "%d/%m/%Y"
|
a, "%d/%m/%Y"
|
||||||
).date()
|
).date()
|
||||||
@@ -432,7 +431,7 @@ def main(
|
|||||||
else:
|
else:
|
||||||
oop = []
|
oop = []
|
||||||
|
|
||||||
print(f"{worker} OOP {oop}")
|
#print(f"{worker} OOP {oop}")
|
||||||
|
|
||||||
previous_shifts = {}
|
previous_shifts = {}
|
||||||
# if twi:
|
# if twi:
|
||||||
@@ -466,10 +465,10 @@ def main(
|
|||||||
|
|
||||||
shift_fte_overrides = {}
|
shift_fte_overrides = {}
|
||||||
|
|
||||||
if worker_name == "Hadi Mohamed":
|
#if worker_name == "Hadi Mohamed":
|
||||||
shift_fte_overrides = {
|
# shift_fte_overrides = {
|
||||||
"weekend_exeter": 50,
|
# "weekend_exeter": 50,
|
||||||
}
|
# }
|
||||||
#elif worker_name == "Joel Lim":
|
#elif worker_name == "Joel Lim":
|
||||||
# shift_fte_overrides = {
|
# shift_fte_overrides = {
|
||||||
# "plymouth_twilight": 100,
|
# "plymouth_twilight": 100,
|
||||||
@@ -500,7 +499,7 @@ def main(
|
|||||||
shift_fte_overrides=shift_fte_overrides,
|
shift_fte_overrides=shift_fte_overrides,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(w)
|
#print(w)
|
||||||
|
|
||||||
Rota.add_worker(w)
|
Rota.add_worker(w)
|
||||||
leave.load_academy(Rota)
|
leave.load_academy(Rota)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def load_leave(Rota):
|
|||||||
|
|
||||||
if live_rota:
|
if live_rota:
|
||||||
download = s.get(
|
download = s.get(
|
||||||
"https://docs.google.com/spreadsheets/d/e/2PACX-1vQtNrp9F1fjtAh8vFX-W_R3RlD9H-2P_vCLMoHrVKR0e24yabbsdrD65VjwxoHlV1Qnatmw1NeghQHG/pub?gid=1199196967&single=true&output=csv",
|
"https://docs.google.com/spreadsheets/d/e/2PACX-1vSh64r9uUbPsCdhcV7zmZz10gVqIy6rhXFUvDgJxRD5W45IYEaZpKMxAqDsD1ph6dob4AiRzJXVVtOu/pub?gid=580824586&single=true&output=csv",
|
||||||
headers=headers,
|
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)
|
# 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)
|
||||||
@@ -55,10 +55,12 @@ def load_leave(Rota):
|
|||||||
n = 1
|
n = 1
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if n < 10:
|
if n < 10:
|
||||||
print(row)
|
pass
|
||||||
|
#print(row)
|
||||||
row_title = row[0]
|
row_title = row[0]
|
||||||
print(row_title)
|
#print(row_title)
|
||||||
row_date = row[0]
|
row_date = row[0]
|
||||||
|
row_extra = row[1]
|
||||||
r = row[2:]
|
r = row[2:]
|
||||||
# header, extract names
|
# header, extract names
|
||||||
if n == 1:
|
if n == 1:
|
||||||
@@ -210,6 +212,14 @@ 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 row_title == "Weekends/Twilights":
|
||||||
|
if lower_item:
|
||||||
|
worker["site"] = lower_item
|
||||||
|
|
||||||
|
|
||||||
|
#elif row_title == "Academy":
|
||||||
|
# pass
|
||||||
|
|
||||||
elif re.match(date_re, row_date) is not None:
|
elif re.match(date_re, row_date) is not None:
|
||||||
if lower_item != "":
|
if lower_item != "":
|
||||||
try:
|
try:
|
||||||
@@ -258,70 +268,79 @@ def load_leave(Rota):
|
|||||||
return workers
|
return workers
|
||||||
|
|
||||||
|
|
||||||
# load_leave()
|
|
||||||
|
|
||||||
def load_academy(Rota):
|
def load_academy(Rota):
|
||||||
|
"""Load academy month blocks from the main published CSV and register avoids.
|
||||||
|
|
||||||
|
Spreadsheet layout expected:
|
||||||
|
- Column A: row title (should be 'Academy' for relevant rows)
|
||||||
|
- Column B: start date for the month (e.g. '07/09/2026')
|
||||||
|
- Columns C...: worker columns matching the header row (first CSV row's columns C...)
|
||||||
|
Cells containing the word 'academy' (case-insensitive) mark that worker as being in academy
|
||||||
|
for that month.
|
||||||
"""
|
"""
|
||||||
Some trainees spend month blocks in the academy. This function loads that information from a google sheet that published it as a csv file.
|
# Use the same published CSV as load_leave (main sheet)
|
||||||
"""
|
url = (
|
||||||
url = "https://docs.google.com/spreadsheets/d/e/2PACX-1vQtNrp9F1fjtAh8vFX-W_R3RlD9H-2P_vCLMoHrVKR0e24yabbsdrD65VjwxoHlV1Qnatmw1NeghQHG/pub?gid=1113837782&single=true&output=csv"
|
"https://docs.google.com/spreadsheets/d/e/2PACX-1vSh64r9uUbPsCdhcV7zmZz10gVqIy6rhXFUvDgJxRD5W45IYEaZpKMxAqDsD1ph6dob4AiRzJXVVtOu/pub?gid=580824586&single=true&output=csv"
|
||||||
|
)
|
||||||
|
|
||||||
with Session() as s:
|
with Session() as s:
|
||||||
headers = {"Cache-Control": "no-cache", "Pragma": "no-cache"}
|
headers = {"Cache-Control": "no-cache", "Pragma": "no-cache"}
|
||||||
download = s.get(url, headers=headers)
|
download = s.get(url, headers=headers)
|
||||||
decoded = download.content.decode("utf-8")
|
decoded = download.content.decode("utf-8")
|
||||||
|
|
||||||
reader = csv.reader(decoded.splitlines(), delimiter=',')
|
reader = csv.reader(decoded.splitlines(), delimiter=",")
|
||||||
|
|
||||||
|
names = []
|
||||||
|
month_starts: list[datetime.date] = []
|
||||||
|
month_cells: list[list[str]] = []
|
||||||
|
|
||||||
n = 0
|
n = 0
|
||||||
names = []
|
|
||||||
# collect date rows (these are the month start boundaries) and their cells
|
|
||||||
date_rows: list[datetime.date] = []
|
|
||||||
date_row_cells: list[list[str]] = []
|
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
n += 1
|
n += 1
|
||||||
if n == 1:
|
|
||||||
# header row: first cell is column title, remaining are worker names
|
|
||||||
names = [c.strip() for c in row[1:]]
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
row_title = row[0].strip()
|
# header row: collect worker names from column C onwards
|
||||||
cells = row[1:]
|
if n == 1:
|
||||||
|
# some files may have two leading columns (like 'Email Address' and blank/extra), so take from index 2
|
||||||
|
names = [c.strip() for c in (row[2:] if len(row) > 2 else row[1:])]
|
||||||
|
continue
|
||||||
|
|
||||||
# If this row is a date row like '02/03/2026' then treat it as a month-start boundary
|
row_title = row[0].strip() if len(row) > 0 else ""
|
||||||
if re.match(date_re, row_title):
|
start_cell = row[1].strip() if len(row) > 1 else ""
|
||||||
|
cells = row[2:] if len(row) > 2 else []
|
||||||
|
|
||||||
|
# Only treat rows where the first column mentions 'Academy' (some sheets may include other rows)
|
||||||
|
if row_title and "academy" in row_title.lower():
|
||||||
|
# parse start date
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
d = datetime.strptime(row_title, "%d/%m/%Y").date()
|
start_date = datetime.strptime(start_cell, "%d/%m/%Y").date()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
d = datetime.strptime(row_title, "%d/%m/%y").date()
|
start_date = datetime.strptime(start_cell, "%d/%m/%y").date()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# skip rows without a valid date
|
||||||
continue
|
continue
|
||||||
|
|
||||||
date_rows.append(d)
|
month_starts.append(start_date)
|
||||||
# normalize cells length to match names
|
# normalize cell list to names length
|
||||||
row_cells = [ (cells[i] if i < len(cells) else "").strip() for i in range(len(names)) ]
|
row_cells = [(cells[i] if i < len(cells) else "").strip() for i in range(len(names))]
|
||||||
date_row_cells.append(row_cells)
|
month_cells.append(row_cells)
|
||||||
|
|
||||||
# If no date rows found, nothing to do
|
if not month_starts:
|
||||||
if not date_rows:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Identify twilight shift names available in the rota
|
# determine twilight shifts available
|
||||||
twilight_shift_names = [s.name for s in Rota.get_shifts() if "twilight" in s.name]
|
twilight_shift_names = [s.name for s in Rota.get_shifts() if "twilight" in s.name]
|
||||||
|
|
||||||
# For each date boundary, compute its end (next boundary - 1 day) and register avoids where cell=='academy'
|
# for each month start, compute end date and register avoids
|
||||||
for idx, start_date in enumerate(date_rows):
|
for idx, start_date in enumerate(month_starts):
|
||||||
if idx + 1 < len(date_rows):
|
if idx + 1 < len(month_starts):
|
||||||
end_date = date_rows[idx + 1] - timedelta(days=1)
|
end_date = month_starts[idx + 1] - timedelta(days=1)
|
||||||
else:
|
else:
|
||||||
end_date = Rota.rota_end_date
|
end_date = Rota.rota_end_date
|
||||||
|
|
||||||
cells = date_row_cells[idx]
|
cells = month_cells[idx]
|
||||||
for i, cell in enumerate(cells):
|
for i, cell in enumerate(cells):
|
||||||
if i >= len(names):
|
if i >= len(names):
|
||||||
break
|
break
|
||||||
@@ -341,7 +360,7 @@ def load_academy(Rota):
|
|||||||
sh = Rota.get_shift_by_name(sh_name)
|
sh = Rota.get_shift_by_name(sh_name)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if hasattr(worker, "site") and worker.site in sh.sites:
|
if hasattr(worker, "site") and worker.site in getattr(sh, "sites", []):
|
||||||
candidate_shifts.append(sh_name)
|
candidate_shifts.append(sh_name)
|
||||||
elif sh_name.startswith(f"{worker.site}_"):
|
elif sh_name.startswith(f"{worker.site}_"):
|
||||||
candidate_shifts.append(sh_name)
|
candidate_shifts.append(sh_name)
|
||||||
@@ -349,7 +368,12 @@ def load_academy(Rota):
|
|||||||
if not candidate_shifts:
|
if not candidate_shifts:
|
||||||
candidate_shifts = twilight_shift_names.copy()
|
candidate_shifts = twilight_shift_names.copy()
|
||||||
|
|
||||||
# register as a date range with a reason 'academy'
|
#print(f"Academy: {worker.name} on academy from {start_date} to {end_date}, avoiding shifts: {candidate_shifts}")
|
||||||
|
|
||||||
Rota.add_avoid_shifts_for_workers_on_dates(
|
Rota.add_avoid_shifts_for_workers_on_dates(
|
||||||
names=[worker.name], shifts=candidate_shifts, start_date=start_date, end_date=end_date, reason="academy"
|
names=[worker.name],
|
||||||
|
shifts=candidate_shifts,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
reason="academy",
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user