numberous fixes

This commit is contained in:
Ross
2024-01-07 17:52:36 +00:00
parent 5d55ed77a8
commit a7e1fa7171
4 changed files with 115 additions and 83 deletions
+25 -1
View File
@@ -546,4 +546,28 @@ function hsv2rgb(h, s, v) {
return '#' + rgb.map(function (x) { return '#' + rgb.map(function (x) {
return ("0" + Math.round(x * 255).toString(16)).slice(-2); return ("0" + Math.round(x * 255).toString(16)).slice(-2);
}).join(''); }).join('');
}; };
//$("#workers-json-target").append(syntaxHighlight(JSON.stringify(JSON.parse(($("#worker_details").data("workers"))))))
function syntaxHighlight(json) {
if (typeof json != 'string') {
json = JSON.stringify(json, undefined, 2);
}
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
+8
View File
@@ -0,0 +1,8 @@
# Load uk based bank holidays
from govuk_bank_holidays.bank_holidays import BankHolidays
bank_holidays = BankHolidays()
bank_holiday_map = {}
for bank_holiday in bank_holidays.get_holidays(division="england-and-wales"):
bank_holiday_map[bank_holiday["date"]] = bank_holiday["title"]
+68 -71
View File
@@ -13,9 +13,11 @@ import datetime
from pyomo.environ import * from pyomo.environ import *
from pyomo.opt import SolverFactory from pyomo.opt import SolverFactory
import urllib.parse
from rota.workers import Worker from rota.workers import Worker
from rota.console import console from rota.console import console
from rota.bank_holidays import bank_holiday_map
import json import json
@@ -29,6 +31,7 @@ import sys
from rich.pretty import pprint from rich.pretty import pprint
from rich.progress import track from rich.progress import track
from rich.panel import Panel from rich.panel import Panel
from rich import print
import logging import logging
@@ -47,16 +50,6 @@ days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
full_weekend_count = 2 full_weekend_count = 2
# Load uk based bank holidays
from govuk_bank_holidays.bank_holidays import BankHolidays
bank_holidays = BankHolidays()
bank_holiday_map = {}
logging.debug("Creating bank holiday map")
for bank_holiday in bank_holidays.get_holidays(division="england-and-wales"):
bank_holiday_map[bank_holiday["date"]] = bank_holiday["title"]
logging.debug(bank_holiday_map)
SHIFT_BOUNDS = { SHIFT_BOUNDS = {
"bank_holiday": (0, 9), "bank_holiday": (0, 9),
@@ -192,8 +185,8 @@ class RotaBuilder(object):
use_bank_holiday_extra: bool = False, use_bank_holiday_extra: bool = False,
SHIFT_BOUNDS=SHIFT_BOUNDS, SHIFT_BOUNDS=SHIFT_BOUNDS,
name: str = "", name: str = "",
bank_holidays: dict[datetime.date, str] = bank_holiday_map,
): ):
self.name = name self.name = name
console.print( console.print(
@@ -207,7 +200,6 @@ class RotaBuilder(object):
justify="left", justify="left",
) )
self.SHIFT_BOUNDS = SHIFT_BOUNDS self.SHIFT_BOUNDS = SHIFT_BOUNDS
self.shifts: List[SingleShift] = [] # type List[SingleShift] self.shifts: List[SingleShift] = [] # type List[SingleShift]
@@ -249,7 +241,6 @@ class RotaBuilder(object):
"prevent_monday_and_tuesday_after_full_weekends": [], "prevent_monday_and_tuesday_after_full_weekends": [],
"prevent_fridays_before_full_weekends": [], "prevent_fridays_before_full_weekends": [],
"prevent_thursdays_before_full_weekends": [], "prevent_thursdays_before_full_weekends": [],
"avoid_st2_first_month": False,
"hard_constrain_pair_separation": False, "hard_constrain_pair_separation": False,
"avoid_shifts_by_grades": [], "avoid_shifts_by_grades": [],
"avoid_shifts_by_worker_names": [], "avoid_shifts_by_worker_names": [],
@@ -257,6 +248,8 @@ class RotaBuilder(object):
self.results = None self.results = None
self.warnings: list[tuple[str, str]] = []
self.ignore_valid_shifts = False self.ignore_valid_shifts = False
self.set_rota_dates(start_date, weeks_to_rota) self.set_rota_dates(start_date, weeks_to_rota)
@@ -264,8 +257,9 @@ class RotaBuilder(object):
self.run_start_time: datetime.datetime | None = None self.run_start_time: datetime.datetime | None = None
self.run_end_time: datetime.datetime | None = None self.run_end_time: datetime.datetime | None = None
def set_rota_dates(self, start_date: datetime.date, weeks_to_rota: int): self.bank_holidays = bank_holidays
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
self.days = days self.days = days
@@ -290,7 +284,7 @@ class RotaBuilder(object):
# Generate a map for week, day combinations to a given date # Generate a map for week, day combinations to a given date
self.week_day_date_map = {} self.week_day_date_map = {}
n = 0 n: int = 0
for week, day in self.get_week_day_combinations(): for week, day in self.get_week_day_combinations():
d = self.start_date + datetime.timedelta(n) d = self.start_date + datetime.timedelta(n)
self.week_day_date_map[(week, day)] = d self.week_day_date_map[(week, day)] = d
@@ -423,9 +417,8 @@ class RotaBuilder(object):
solve=True, solve=True,
debug_if_fail: bool = False, debug_if_fail: bool = False,
export=False, export=False,
solver="cbc" solver="cbc",
): ):
self.run_start_time = datetime.datetime.now() self.run_start_time = datetime.datetime.now()
self.build_shifts() self.build_shifts()
@@ -433,7 +426,9 @@ class RotaBuilder(object):
self.build_model() self.build_model()
if solve: if solve:
self.solve_model(options=options, debug_if_fail=debug_if_fail, solver=solver) self.solve_model(
options=options, debug_if_fail=debug_if_fail, solver=solver
)
self.run_end_time = datetime.datetime.now() self.run_end_time = datetime.datetime.now()
@@ -1206,21 +1201,24 @@ class RotaBuilder(object):
except ValueError: except ValueError:
# Occurs if there are no shifts within a defined block # Occurs if there are no shifts within a defined block
# TODO: test if this breaks (and we should check rathar than except) # TODO: test if this breaks (and we should check rathar than except)
logging.debug( self.add_warning(
"max shifts per month constraint"
f"Failed to constrain max_shifts_per_month for worker: {worker.name}" f"Failed to constrain max_shifts_per_month for worker: {worker.name}"
) )
pass pass
for constraint_dict in self.constraint_options["avoid_shifts_by_worker_names"]: for constraint_dict in self.constraint_options[
"avoid_shifts_by_worker_names"
]:
if invalid_shifts := set(constraint_dict["shifts"]).difference( if invalid_shifts := set(constraint_dict["shifts"]).difference(
set(self.get_shift_names()) set(self.get_shift_names())
): ):
message = f"avoid shifts by worker constraint contains a non existent shift: {invalid_shifts}"
if self.ignore_valid_shifts: if self.ignore_valid_shifts:
self.add_warning("Non existent shift", message)
continue # Skip if we don't want to raise an error continue # Skip if we don't want to raise an error
else: else:
raise InvalidShift( raise InvalidShift(message)
f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}"
)
if worker.name in constraint_dict["names"]: if worker.name in constraint_dict["names"]:
self.model.constraints.add( self.model.constraints.add(
@@ -1237,18 +1235,18 @@ class RotaBuilder(object):
if invalid_shifts := set(constraint_dict["shifts"]).difference( if invalid_shifts := set(constraint_dict["shifts"]).difference(
set(self.get_shift_names()) set(self.get_shift_names())
): ):
message = f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}"
if self.ignore_valid_shifts: if self.ignore_valid_shifts:
self.add_warning("Non existent shift", message)
continue # Skip if we don't want to raise an error continue # Skip if we don't want to raise an error
else: else:
raise InvalidShift( raise InvalidShift(message)
f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}"
)
if invalid_grades := set(constraint_dict["grades"]).difference( if invalid_grades := set(constraint_dict["grades"]).difference(
set(self.get_worker_grades()) set(self.get_worker_grades())
): ):
raise ValueError( message = f"avoid shifts by grade constraint contains a non existent worker grade: {invalid_grades}"
f"avoid shifts by grade constraint contains a non existent worker grade: {invalid_grades}" self.add_warning("Non existent shift", message)
) raise ValueError(message)
if worker.grade in constraint_dict["grades"]: if worker.grade in constraint_dict["grades"]:
self.model.constraints.add( self.model.constraints.add(
@@ -1261,24 +1259,6 @@ class RotaBuilder(object):
) )
) )
# sys.exit(0)
if self.constraint_options["avoid_st2_first_month"] and worker.grade == 2:
# Avoid ST1s on the first month
try:
self.model.constraints.add(
0
== sum(
self.model.works[worker.id, week, day, shift]
for week, day, shift in self.get_all_shiftname_combinations()
if week in (1, 2, 3, 4)
and shift in ("night_weekday", "night_weekend")
)
)
except ValueError as e:
print("Failure setting constraint", "avoid_st2_first_month")
print(e)
# Count number of weekends an worker works # Count number of weekends an worker works
# if self.constraint_options["balance_weekends"]: # if self.constraint_options["balance_weekends"]:
self.model.constraints.add( self.model.constraints.add(
@@ -1301,7 +1281,6 @@ class RotaBuilder(object):
if ( if (
worker.site in shift.sites worker.site in shift.sites
): # Each site specfies which sites self.workers can fullfill it ): # Each site specfies which sites self.workers can fullfill it
# calaculate the total number of shifts that need assigning over the rota # calaculate the total number of shifts that need assigning over the rota
# total_shifts = (len(self.weeks) * len(shift.days) * # total_shifts = (len(self.weeks) * len(shift.days) *
total_shifts = ( total_shifts = (
@@ -1458,7 +1437,6 @@ class RotaBuilder(object):
) )
if self.constraint_options["balance_bank_holidays"]: if self.constraint_options["balance_bank_holidays"]:
extra_bank_holiday = 0 extra_bank_holiday = 0
if self.use_bank_holiday_extra: if self.use_bank_holiday_extra:
extra_bank_holiday = worker.bank_holiday_extra extra_bank_holiday = worker.bank_holiday_extra
@@ -1468,7 +1446,7 @@ class RotaBuilder(object):
== sum( == sum(
self.model.works[worker.id, week, day, shift] self.model.works[worker.id, week, day, shift]
for week, day, shift in self.get_all_shiftname_combinations() for week, day, shift in self.get_all_shiftname_combinations()
if self.week_day_date_map[(week, day)] in bank_holiday_map if self.week_day_date_map[(week, day)] in self.bank_holidays
) )
+ extra_bank_holiday + extra_bank_holiday
) )
@@ -1569,7 +1547,6 @@ class RotaBuilder(object):
worker.weekend_shift_target_number = weekend_shift_target_number worker.weekend_shift_target_number = weekend_shift_target_number
if self.constraint_options["balance_weekends"]: if self.constraint_options["balance_weekends"]:
if weekend_shift_target_number > 0: if weekend_shift_target_number > 0:
self.model.constraints.add( self.model.constraints.add(
self.model.weekend_shift_count_t1[worker.id] self.model.weekend_shift_count_t1[worker.id]
@@ -1929,7 +1906,6 @@ class RotaBuilder(object):
for n in track( for n in track(
range(len(weeks_days)), description="Generate week/day constraints" range(len(weeks_days)), description="Generate week/day constraints"
): ):
week, day = weeks_days[n] week, day = weeks_days[n]
pre_map = [] pre_map = []
@@ -2100,7 +2076,6 @@ class RotaBuilder(object):
workers = worker_pairs workers = worker_pairs
if self.get_shift_names_by_week_day(week, day): if self.get_shift_names_by_week_day(week, day):
# Unable to work (hard constraint not preference) # Unable to work (hard constraint not preference)
self.model.constraints.add( self.model.constraints.add(
self.model.available[worker.id, week, day] self.model.available[worker.id, week, day]
@@ -2309,7 +2284,6 @@ class RotaBuilder(object):
def define_objectives(self): def define_objectives(self):
# Define an objective function with model as input, to pass later # Define an objective function with model as input, to pass later
def obj_rule(m): def obj_rule(m):
# c = len(workers) # c = len(workers)
balance_modifier_constant = 1 balance_modifier_constant = 1
@@ -2483,6 +2457,10 @@ class RotaBuilder(object):
# add objective function to the model. rule (pass function) or expr (pass expression directly) # add objective function to the model. rule (pass function) or expr (pass expression directly)
self.model.obj = Objective(rule=obj_rule, sense=minimize) self.model.obj = Objective(rule=obj_rule, sense=minimize)
def add_warning(self, warning_type: str, message: str):
print(f"[bold red]WARNING:[/bold red] {warning_type} - {message}")
self.warnings.append((warning_type, message))
def add_worker(self, worker: Worker) -> None: def add_worker(self, worker: Worker) -> None:
"""Add a worker to the rota """Add a worker to the rota
@@ -2509,8 +2487,8 @@ class RotaBuilder(object):
if not self.workers: if not self.workers:
raise NoWorkers("Workers must be added prior to calling build_workers") raise NoWorkers("Workers must be added prior to calling build_workers")
self.workers_id_map = {} self.workers_id_map: dict[str, Worker] = {}
self.workers_name_map = {} self.workers_name_map: dict[str, Worker] = {}
for worker in track(self.workers, description="Building workers"): for worker in track(self.workers, description="Building workers"):
worker.load_rota(self) worker.load_rota(self)
@@ -2525,6 +2503,11 @@ class RotaBuilder(object):
) )
self.workers_name_map[worker.name] = worker self.workers_name_map[worker.name] = worker
if worker.site not in self.sites:
raise ValueError(
f"Worker with name '{worker.name}' ({worker.id}) has no valid shifts (site: {worker.site})"
)
self.workers = sorted(self.workers) self.workers = sorted(self.workers)
self.full_time_equivalent = sum(w.fte_adj for w in self.workers) self.full_time_equivalent = sum(w.fte_adj for w in self.workers)
@@ -2655,7 +2638,7 @@ class RotaBuilder(object):
self.week_day_shifts_dict[(week, day)] = set() self.week_day_shifts_dict[(week, day)] = set()
for s in self.shifts: for s in self.shifts:
if s.bank_holidays_only: if s.bank_holidays_only:
if self.week_day_date_map[(week, day)] in bank_holiday_map: if self.week_day_date_map[(week, day)] in self.bank_holidays:
print(s.name, self.week_day_date_map[(week, day)]) print(s.name, self.week_day_date_map[(week, day)])
self.week_day_shift_product.append((week, day, s.name)) self.week_day_shift_product.append((week, day, s.name))
self.week_day_shiftclass_product.append((week, day, s)) self.week_day_shiftclass_product.append((week, day, s))
@@ -2870,28 +2853,31 @@ class RotaBuilder(object):
s.extend(self.shifts_to_force_as_blocks()) s.extend(self.shifts_to_force_as_blocks())
return s return s
def get_all_workers(self) -> List[Worker]:
return self.workers
def get_worker_by_id(self, worker_id: str) -> Worker: def get_worker_by_id(self, worker_id: str) -> Worker:
return self.workers_id_map[worker_id] return self.workers_id_map[worker_id]
def get_worker_by_name(self, name: str) -> Worker: def get_worker_by_name(self, name: str) -> Worker:
return self.workers_name_map[name] return self.workers_name_map[name]
def get_workers_by_group(self) -> Dict[str, Worker]: def get_workers_by_group(self) -> dict[str, list[Worker]]:
group_workers = defaultdict(list) group_workers: dict[str, list[Worker]] = defaultdict(list)
for worker in self.workers: for worker in self.workers:
group_workers[worker.site].append(worker) group_workers[worker.site].append(worker)
return group_workers return group_workers
def get_workers_by_remote_group(self) -> Dict[str, Worker]: def get_workers_by_remote_group(self) -> dict[str, list[Worker]]:
group_workers = defaultdict(list) group_workers: dict[str, list[Worker]] = defaultdict(list)
for worker in self.workers: for worker in self.workers:
group_workers[worker.remote_site].append(worker) group_workers[worker.remote_site].append(worker)
return group_workers return group_workers
def get_workers_by_grade(self) -> Dict[str, Worker]: def get_workers_by_grade(self) -> Dict[int, list[Worker]]:
group_workers = defaultdict(list) group_workers: dict[int, list[Worker]] = defaultdict(list)
for worker in self.workers: for worker in self.workers:
group_workers[worker.grade].append(worker) group_workers[worker.grade].append(worker)
@@ -2904,7 +2890,6 @@ class RotaBuilder(object):
return self.full_time_equivalent return self.full_time_equivalent
def get_worker_details(self): def get_worker_details(self):
w = defaultdict(list) w = defaultdict(list)
for worker in self.workers: for worker in self.workers:
# print(worker) # print(worker)
@@ -2926,18 +2911,19 @@ class RotaBuilder(object):
return [ return [
(week, day) (week, day)
for week, day in self.get_week_day_combinations() for week, day in self.get_week_day_combinations()
if self.week_day_date_map[(week, day)] in bank_holiday_map if self.week_day_date_map[(week, day)] in self.bank_holidays
] ]
def get_week_start_date(self, week: int): def get_week_start_date(self, week: int):
return self.start_date + datetime.timedelta(weeks=week) return self.start_date + datetime.timedelta(weeks=week)
# RESULTS # RESULTS
def export_rota_to_html(self, filename: str = "rota", folder=None, timestamp_filename=True): def export_rota_to_html(
self, filename: str = "rota", folder=None, timestamp_filename=True
):
if timestamp_filename: if timestamp_filename:
filename = f"{filename}_{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}" filename = f"{filename}_{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
if folder is not None: if folder is not None:
output_file = Path("output", folder, f"{filename}.html") output_file = Path("output", folder, f"{filename}.html")
else: else:
@@ -2947,6 +2933,8 @@ class RotaBuilder(object):
output_file.parent.mkdir(parents=True, exist_ok=True) output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w") as f: with open(output_file, "w") as f:
f.write(self.get_worker_timetable_html(True)) f.write(self.get_worker_timetable_html(True))
url = urllib.parse.quote(str(output_file.resolve()))
print(f"Rota exported [bold blue]('[link url={url}]{url}')[/bold blue]")
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")
@@ -3094,7 +3082,7 @@ class RotaBuilder(object):
d = self.start_date + datetime.timedelta(n) d = self.start_date + datetime.timedelta(n)
th_class = "date" th_class = "date"
if d in bank_holiday_map: if d in self.bank_holidays:
th_class = th_class + " bank-holiday" th_class = th_class + " bank-holiday"
date_row.append( date_row.append(
@@ -3105,7 +3093,7 @@ class RotaBuilder(object):
current_site = "" current_site = ""
for worker in self.workers: for worker in self.get_all_workers():
if worker.site != current_site: if worker.site != current_site:
try: try:
timetable.append( timetable.append(
@@ -3165,10 +3153,10 @@ class RotaBuilder(object):
title = f"{title} / {unavailable_reason}" title = f"{title} / {unavailable_reason}"
bank_holiday = "" bank_holiday = ""
if d in bank_holiday_map: if d in self.bank_holidays:
title = f"{title} [Bank Holiday]" title = f"{title} [Bank Holiday]"
css_class = " ".join((css_class, "bank-holiday")) css_class = " ".join((css_class, "bank-holiday"))
bank_holiday = f" data-bank-holiday='{bank_holiday_map[d]}'" bank_holiday = f" data-bank-holiday='{self.bank_holidays[d]}'"
requests = "" requests = ""
if (worker.id, week, day) in self.work_requests_map: if (worker.id, week, day) in self.work_requests_map:
@@ -3203,6 +3191,7 @@ class RotaBuilder(object):
data-fte='{fte}' data-fte_adj='{fte_adj}' data-fte='{fte}' data-fte_adj='{fte_adj}'
data-start_date='{start_date}' data-start_date='{start_date}'
data-end_date='{end_date}' data-end_date='{end_date}'
data-oops='{oops}'
data-worker-targets='{targets}' data-shift-counts='{worker_shift_counts}' data-worker-targets='{targets}' data-shift-counts='{worker_shift_counts}'
data-weekend-target='{weekend_target}' data-weekend-target='{weekend_target}'
data-remote-site='{remote_site}' data-remote-site='{remote_site}'
@@ -3220,6 +3209,7 @@ class RotaBuilder(object):
fte_adj=worker.fte_adj, fte_adj=worker.fte_adj,
start_date=worker.calculated_start_date, start_date=worker.calculated_start_date,
end_date=worker.calculated_end_date, end_date=worker.calculated_end_date,
oops=json.dumps(json.dumps(worker.oop, default=str)),
targets=worker_targets, targets=worker_targets,
worker_shift_counts=json.dumps(shift_count_dict), worker_shift_counts=json.dumps(shift_count_dict),
weekend_target=worker.weekend_shift_target_number, weekend_target=worker.weekend_shift_target_number,
@@ -3302,6 +3292,13 @@ class RotaBuilder(object):
</div> </div>
</details> </details>
<details> <details>
<summary><h2>Worker details</h2></summary>
<div id="worker_details" data-workers='{json.dumps([json.loads(worker.model_dump_json()) for worker in self.get_all_workers()])}'>
<span id="workers-json-target"></span>
</div>
</details>
<details>
<summary><h2>Output</h2></summary> <summary><h2>Output</h2></summary>
<pre> <pre>
{result_string} {result_string}
+14 -11
View File
@@ -68,7 +68,7 @@ class Worker(BaseModel):
# rules is easier. # rules is easier.
grade: int grade: int
# We can either have a user generated ID # We can either have a user generated ID
id: Optional[int] = None id: Optional[int| uuid.UUID] = None
fte: int = 100 fte: int = 100
nwds: list[NonWorkingDays] = [] nwds: list[NonWorkingDays] = []
start_date: datetime.date | None = None start_date: datetime.date | None = None
@@ -108,30 +108,32 @@ class Worker(BaseModel):
# #
# # days_to_work = Rota.rota_days_length # # days_to_work = Rota.rota_days_length
def load_rota(self, Rota): def load_rota(self, Rota: "RotaBuilder"):
self.proportion_rota_to_work = 1 self.proportion_rota_to_work: float = 1
self.shift_target_number = defaultdict(int) self.shift_target_number: dict[ str, int ] = defaultdict(int)
if self.id is None: if self.id is None:
self.id = uuid.uuid4() self.id = uuid.uuid4()
if self.start_date is not None and self.end_date is not None: if self.start_date is not None and self.end_date is not None:
if self.start_date >= self.end_date: if self.start_date >= self.end_date:
raise ValueError("End date must be after start date") 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 self.name == "Steph bailey":
print(self.start_date, self.end_date)
print(self.oop)
# if no start date default to the start of the rota # if no start date default to the start of the rota
if self.start_date is None: if self.start_date is None:
self.calculated_start_date = Rota.start_date self.calculated_start_date = Rota.start_date
else: else:
self.calculated_start_date = self.start_date
# ? test if start date is valid # ? test if start date is valid
if self.start_date < Rota.start_date: 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: else:
self.calculated_start_date = self.start_date pass
if self.end_date is None: if self.end_date is None:
self.calculated_end_date = Rota.rota_end_date self.calculated_end_date = Rota.rota_end_date
@@ -256,6 +258,7 @@ class Worker(BaseModel):
if self.fte_adj > 100: if self.fte_adj > 100:
# Shouldn't happen ? bug if it does # 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)) raise ValueError("{} : fte_ajd = {}".format(self.name, self.fte_adj))
# TODO: this has already been validated, consider moving # TODO: this has already been validated, consider moving