diff --git a/run.py b/run.py index 1eeb3ff..01aa40c 100644 --- a/run.py +++ b/run.py @@ -47,7 +47,7 @@ Rota.constraint_options["constrain_time_off_after_nights"] = True Rota.constraint_options["balance_nights_across_sites"] = True Rota.constraint_options["balance_blocks"] = True Rota.constraint_options["balance_weekends"] = True -Rota.constraint_options["balance_shifts"] = True +Rota.constraint_options["balance_shifts_over_workers"] = True Rota.constraint_options["balance_bank_holidays"] = True Rota.constraint_options["avoid_st2_first_month"] = False Rota.constraint_options["hard_constrain_pair_separation"] = True diff --git a/runfixnights.py b/runfixnights.py index b85c533..cd2ea2c 100644 --- a/runfixnights.py +++ b/runfixnights.py @@ -47,7 +47,7 @@ Rota.constraint_options["constrain_time_off_after_nights"] = True Rota.constraint_options["balance_nights_across_sites"] = True Rota.constraint_options["balance_blocks"] = True Rota.constraint_options["balance_weekends"] = True -Rota.constraint_options["balance_shifts"] = True +Rota.constraint_options["balance_shifts_over_workers"] = True Rota.constraint_options["balance_bank_holidays"] = True Rota.constraint_options["avoid_st2_first_month"] = False Rota.constraint_options["hard_constrain_pair_separation"] = True diff --git a/shifts.py b/shifts.py index 78d85cf..5d72cb8 100644 --- a/shifts.py +++ b/shifts.py @@ -1,6 +1,6 @@ import datetime import itertools -from typing import Iterable, List, Sequence, Tuple +from typing import Iterable, List, Sequence, Tuple, Set import datetime @@ -104,12 +104,12 @@ class RotaBuilder(object): def __init__( self, - start_date: datetime.datetime, + start_date: datetime.datetime = datetime.datetime.now(), weeks_to_rota: int = 26, balance_offset_modifier: int = 1, ltft_balance_offset: int = 1, max_night_frequency: int = 2, - max_weekend_frequency: int = 2, # Requires balance weekends + max_weekend_frequency: int = 1, # Requires balance weekends use_previous_shifts: bool = False, use_shift_balance_extra: bool = False, use_bank_holiday_extra: bool = False, @@ -171,11 +171,12 @@ class RotaBuilder(object): "balance_bank_holidays": True, "balance_blocks": True, "balance_shifts": True, - "minimise_shift_diffs": False, # less sophisticated version of balance_shifts + "balance_shifts_over_workers": True, + "minimise_shift_diffs": False, # less sophisticated version of balance_shifts_over_workers "balance_weekends": True, "max_weekends": 100, - "max_shifts_per_week": 4, - "max_shifts_per_month": 12, + "max_shifts_per_week": 7, + "max_shifts_per_month": 40, "prevent_monday_after_full_weekends": [], "prevent_monday_and_tuesday_after_full_weekends": [], "prevent_fridays_before_full_weekends": [], @@ -226,6 +227,14 @@ class RotaBuilder(object): if not results.solver.status: sys.exit(0) + def build_and_solve(self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}): + self.build_shifts() + self.build_workers() + self.build_model() + + self.solve_model(options=options) + + def build_model(self): # Initialize model self.model = ConcreteModel() @@ -234,10 +243,9 @@ class RotaBuilder(object): # NOTE: this will assign to all possible shift combinations (which we probably don't want) self.model.works = Var( ( - (worker.id, week, day, shift) + (worker.id, week, day, shiftname) for worker in self.workers - for week, day in self.get_week_day_combinations() - for shift in self.get_shift_names() + for week, day, shiftname in self.get_all_shiftname_combinations() ), within=Binary, initialize=0, @@ -352,6 +360,7 @@ class RotaBuilder(object): initialize=0, ) + self.model.shift_count = Var( ( (worker.id, shift) @@ -379,20 +388,40 @@ class RotaBuilder(object): initialize=0, ) - if self.constraint_options["balance_shifts"]: - - self.model.shift_count_t1 = Var( + if self.constraint_options["balance_shifts_over_workers"]: + self.model.worker_shift_count_t1 = Var( ((worker.id) for worker in self.workers), domain=NonNegativeReals, initialize=0, ) - self.model.shift_count_t2 = Var( + self.model.worker_shift_count_t2 = Var( ((worker.id) for worker in self.workers), domain=NonNegativeReals, initialize=0, ) + if self.constraint_options["balance_shifts"]: + self.model.shift_count_t1 = Var( + ( + (worker.id, shift.name) + for worker in self.workers + for shift in self.get_shifts() + ), + domain=NonNegativeReals, + initialize=0, + ) + + self.model.shift_count_t2 = Var( + ( + (worker.id, shift.name) + for worker in self.workers + for shift in self.get_shifts() + ), + domain=NonNegativeReals, + initialize=0, + ) + if self.constraint_options["balance_nights"]: # We also try to even out the night shifts seperately self.model.night_shift_count = Var( @@ -453,21 +482,21 @@ class RotaBuilder(object): # initialize=0, # ) + + self.model.works_weekend = Var( + ((worker.id, week) for worker in self.workers for week in self.weeks), + domain=Binary, + initialize=0, + ) + + self.model.worker_weekend_count = Var( + ((worker.id) for worker in self.workers), + domain=NonNegativeIntegers, + initialize=0, + bounds=SHIFT_BOUNDS["weekend_count"], + ) + if self.constraint_options["balance_weekends"]: - - self.model.works_weekend = Var( - ((worker.id, week) for worker in self.workers for week in self.weeks), - domain=Binary, - initialize=0, - ) - - self.model.worker_weekend_count = Var( - ((worker.id) for worker in self.workers), - domain=NonNegativeIntegers, - initialize=0, - bounds=SHIFT_BOUNDS["weekend_count"], - ) - self.model.weekend_shift_count_t1 = Var( ((worker.id) for worker in self.workers), domain=NonNegativeReals, @@ -591,15 +620,16 @@ class RotaBuilder(object): # self.model.works[worker.id, week, day, shift] # for worker in self.workers if worker.site not in site_required)) + # THIS SHOULD NO LONGER BE REQUIRED # Ensure no workers are assigned to shifts that are not required - for week, day, shift in self.get_not_required_shifts(): - self.model.constraints.add( - 0 - == sum( - self.model.works[worker.id, week, day, shift.name] - for worker in self.workers - ) - ) + #for week, day, shift in self.get_not_required_shifts(): + # self.model.constraints.add( + # 0 + # == sum( + # self.model.works[worker.id, week, day, shift.name] + # for worker in self.workers + # ) + # ) # Constraint: total hours worked hours worked # for worker in self.workers: @@ -820,7 +850,7 @@ class RotaBuilder(object): ] for worker in self.workers ) - <= 1 + <= shift.workers_required ) # Most of our constraints apply per worker @@ -840,10 +870,9 @@ class RotaBuilder(object): self.model.constraints.add( self.constraint_options["max_shifts_per_month"] >= sum( - self.model.works[worker.id, week, day, shift.name] - for week in week_blocks - for day in self.days - for shift in self.get_shifts() + self.model.works[worker.id, week, day, shiftname] + for week, day, shiftname in self.get_all_shiftname_combinations() + if week in week_blocks ) ) @@ -862,14 +891,15 @@ class RotaBuilder(object): except ValueError as e: print("Failure setting constraint", "avoid_st2_first_month") print(e) + # Count number of weekends an worker works - if self.constraint_options["balance_weekends"]: - self.model.constraints.add( - self.model.worker_weekend_count[worker.id] - == sum( - self.model.works_weekend[worker.id, week] for week in self.weeks - ) + #if self.constraint_options["balance_weekends"]: + self.model.constraints.add( + self.model.worker_weekend_count[worker.id] + == sum( + self.model.works_weekend[worker.id, week] for week in self.weeks ) + ) if self.constraint_options["max_weekends"] > -1: self.model.constraints.add( @@ -933,7 +963,9 @@ class RotaBuilder(object): min_shifts, sum( self.model.works[worker.id, week, day, shift.name] - for week, day in self.get_week_day_combinations() + for week, day in self.get_week_day_combinations_for_shift(shift) + #for week, day in self.get_week_day_combinations() + #if shift.name in self.get_shift_names_by_week_day(week, day) ), max_shifts, ) @@ -944,7 +976,8 @@ class RotaBuilder(object): 0 == sum( self.model.works[worker.id, week, day, shift.name] - for week, day in self.get_week_day_combinations() + for week, day in self.get_week_day_combinations_for_shift(shift) + #for week, day in self.get_week_day_combinations() ) ) @@ -954,7 +987,7 @@ class RotaBuilder(object): self.model.shift_count[worker.id, shift.name] == sum( self.model.works[worker.id, week, day, shift.name] - for week, day in self.get_week_day_combinations() + for week, day in self.get_week_day_combinations_for_shift(shift) ) ) @@ -964,16 +997,23 @@ class RotaBuilder(object): - worker.shift_target_number[shift.name] ) - # Define shift_count_t1 and shift_count_t2 constraints for the object + if self.constraint_options["balance_shifts"]: + self.model.constraints.add( + self.model.shift_count_t1[worker.id, shift.name] + - self.model.shift_count_t2[worker.id, shift.name] + == self.model.shift_count_diff[worker.id, shift.name] + ) + + # Define worker_shift_count_t1 and worker_shift_count_t2 constraints for the object # Thus bypassing the need for a quadratic solver # t1-t2 is the target # As the objective is to minimise t1+t2 and t1 and t2 are positive reals # t1+t2 approximates the absolute target (which otherwise requires a quadratic solver) # TODO: quadratic implementation so perfect solutions will be chosen - if self.constraint_options["balance_shifts"]: + if self.constraint_options["balance_shifts_over_workers"]: self.model.constraints.add( - self.model.shift_count_t1[worker.id] - - self.model.shift_count_t2[worker.id] + self.model.worker_shift_count_t1[worker.id] + - self.model.worker_shift_count_t2[worker.id] == sum( (self.model.shift_count_diff[worker.id, shift.name]) * shift.balance_weighting @@ -1081,14 +1121,15 @@ class RotaBuilder(object): # We use a similar method to balance the number of weekends worked # This works as long as weekend shifts are assigned as blocks! - if self.constraint_options["balance_weekends"]: - weekend_shift_target_number = sum( - worker.shift_target_number[shift.name] / len(shift.shift_days) - for shift in self.get_shifts() - if "Sat" in shift.shift_days or "Sun" in shift.shift_days - ) + weekend_shift_target_number = sum( + worker.shift_target_number[shift.name] / len(shift.shift_days) + for shift in self.get_shifts() + if "Sat" in shift.shift_days or "Sun" in shift.shift_days + ) - 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 weekend_shift_target_number > 0: self.model.constraints.add( @@ -1166,38 +1207,40 @@ class RotaBuilder(object): ) ) - if self.constraint_options["balance_weekends"]: - for week_blocks in self.get_week_block_iterator( - self.max_weekend_frequency - ): - # Prevent weekend shifts more than once every n weeks - self.model.constraints.add( - 1 - >= sum( - self.model.works_weekend[worker.id, week] - for week in week_blocks - ) + #if self.constraint_options["balance_weekends"]: + for week_blocks in self.get_week_block_iterator( + self.max_weekend_frequency + ): + # Prevent weekend shifts more than once every n weeks + self.model.constraints.add( + 1 + >= sum( + self.model.works_weekend[worker.id, week] + for week in week_blocks ) + ) for week in self.weeks: self.model.constraints.add( self.constraint_options["max_shifts_per_week"] >= sum( - self.model.works[worker.id, week, day, shift.name] - for day in self.days - for shift in self.get_shifts() + self.model.works[worker.id, week, day, shiftname] + #for shiftname in self.get_shift_names_by_week_day(week, day) + #for day in self.days + for day, shiftname in self.get_day_shiftname_combinations() ) ) - for shift in self.get_shifts_with_constraint("max_2_shifts_per_week"): - self.model.constraints.add( - 2 - >= sum( - self.model.works[worker.id, week, day, shift.name] - for day in self.days - ) - ) + # TODO: generic implementation + #for shift in self.get_shifts_with_constraint("max_2_shifts_per_week"): + # self.model.constraints.add( + # 2 + # >= sum( + # self.model.works[worker.id, week, day, shift.name] + # for day in self.days + # ) + # ) if ( worker.site @@ -1305,24 +1348,26 @@ class RotaBuilder(object): # self.model.works[worker.id, week, day, shift.name] # for day in self.days[5:] # for shift in self.get_shifts()) / 2) - if self.constraint_options["balance_weekends"]: - self.model.constraints.add( - self.model.works_weekend[worker.id, week] - >= sum( - self.model.works[worker.id, week, day, shift.name] - for day in self.days[5:] - for shift in self.get_shifts() - ) - / 2 + #if self.constraint_options["balance_weekends"]: + self.model.constraints.add( + self.model.works_weekend[worker.id, week] + >= sum( + self.model.works[worker.id, week, day, shiftname] + for day, shiftname in self.get_day_shiftname_combinations() if day in self.days[5:] + #for day in self.days[5:] + #for shift in self.get_shifts() ) - self.model.constraints.add( - self.model.works_weekend[worker.id, week] - <= sum( - self.model.works[worker.id, week, day, shift.name] - for day in self.days[5:] - for shift in self.get_shifts() - ) + / 2 + ) + self.model.constraints.add( + self.model.works_weekend[worker.id, week] + <= sum( + self.model.works[worker.id, week, day, shiftname] + for day, shiftname in self.get_day_shiftname_combinations() if day in self.days[5:] + #for day in self.days[5:] + #for shift in self.get_shifts() ) + ) # self.model.constraints.add( # self.model.works_saturday[worker.id, week] == sum( # self.model.works[worker.id, week, "Sat", shift.name] @@ -1393,6 +1438,13 @@ class RotaBuilder(object): n3week, n3day = weeks_days[n] n3 = 0 + # IF paired we check the following against both workers + workers = [worker] + if self.constraint_options["hard_constrain_pair_separation"]: + for worker_pairs in self.worker_pairs: + if worker_pairs[0] == worker: + workers = worker_pairs + if self.get_shift_names_by_week_day(week, day): # Unable to work (hard constraint not preference) self.model.constraints.add( @@ -1426,90 +1478,142 @@ class RotaBuilder(object): ) ) + for constraint_shift in self.get_shifts_with_constraints( + "preclear", "preclear2" + ): + if day in constraint_shift.shift_days: + self.model.constraints.add( + 1 + >= self.model.works[worker.id, week, day, constraint_shift.name] + + sum( + p1 * self.model.works[w.id, pweek, pday, shiftname] + for shiftname in self.get_shift_names_by_week_day(pweek, pday) + #for day, shiftname in self.get_day_shiftname_combinations() if day == pday + if shiftname != constraint_shift.name + for w in workers + ) + ) + + for constraint_shift in self.get_shifts_with_constraint("preclear2"): + if day in constraint_shift.shift_days: + self.model.constraints.add( + 1 + >= self.model.works[worker.id, week, day, constraint_shift.name] + + sum( + p2 * self.model.works[w.id, p2week, p2day, shiftname] + for shiftname in self.get_shift_names_by_week_day(p2week, p2day) + if shiftname != constraint_shift.name + for w in workers + ) + ) + + for constraint_shift in self.get_shifts_with_constraints( + "postclear", "postclear2" + ): + if day in constraint_shift.shift_days: + self.model.constraints.add( + 1 + >= self.model.works[worker.id, week, day, constraint_shift.name] + + sum( + n1 * self.model.works[w.id, nweek, nday, shiftname] + for shiftname in self.get_shift_names_by_week_day(nweek, nday) + if shiftname != constraint_shift.name + for w in workers + ) + ) + + for constraint_shift in self.get_shifts_with_constraints("postclear2"): + if day in constraint_shift.shift_days: + self.model.constraints.add( + 1 + >= self.model.works[worker.id, week, day, constraint_shift.name] + + sum( + n2 * self.model.works[w.id, n2week, n2day, shiftname] + for shiftname in self.get_shift_names_by_week_day(n2week, n2day) + if shiftname != constraint_shift.name + for w in workers + ) + ) + + # NOTE: use pre / post clear instead # if working a night ensure preceeding (1) or subsequent (2) shifts can only be nights if self.constraint_options["constrain_time_off_after_nights"]: for constraint_shift in self.get_shifts_with_constraint("night"): - # Ensure night prior to unavalibity is not assigned - self.model.constraints.add( - self.model.available[worker.id, week, day] - >= self.model.works[ - worker.id, pweek, pday, constraint_shift.name - ] - ) - - # IF paired we check the following against both workers - workers = [worker] - if self.constraint_options["hard_constrain_pair_separation"]: - for worker_pairs in self.worker_pairs: - if worker_pairs[0] == worker: - workers = worker_pairs - - # print("Workers", workers) - - self.model.constraints.add( - 1 - >= self.model.works[ - worker.id, week, day, constraint_shift.name - ] - + sum( - n1 * self.model.works[w.id, nweek, nday, shift] - for shift in self.get_shift_names_by_week_day( - nweek, nday - ) - if shift != constraint_shift.name - for w in workers + if day in constraint_shift.shift_days: + # Ensure night prior to unavalibity is not assigned + self.model.constraints.add( + self.model.available[worker.id, week, day] + >= self.model.works[ + worker.id, pweek, pday, constraint_shift.name + ] ) - ) - self.model.constraints.add( - 1 - >= self.model.works[ - worker.id, week, day, constraint_shift.name - ] - + sum( - n2 * self.model.works[w.id, n2week, n2day, shift] - for shift in self.get_shift_names_by_week_day( - n2week, n2day + # print("Workers", workers) + + self.model.constraints.add( + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + n1 * self.model.works[w.id, nweek, nday, shift] + for shift in self.get_shift_names_by_week_day( + nweek, nday + ) + if shift != constraint_shift.name + for w in workers ) - if shift != constraint_shift.name - for w in workers ) - ) - # self.model.constraints.add( - # 1 >= self.model.works[worker.id, week, day, - # constraint_shift.name] + - # sum(n3 * self.model.works[worker.id, n3week, n3day, - # shift] - # for shift in self.get_shift_names_by_week_day(week, n3day) - # if shift != constraint_shift.name)) - self.model.constraints.add( - 1 - >= self.model.works[ - worker.id, week, day, constraint_shift.name - ] - + sum( - p1 * self.model.works[w.id, pweek, pday, shift] - for shift in self.get_shift_names_by_week_day( - pweek, pday + + self.model.constraints.add( + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + n2 * self.model.works[w.id, n2week, n2day, shift] + for shift in self.get_shift_names_by_week_day( + n2week, n2day + ) + if shift != constraint_shift.name + for w in workers ) - if shift != constraint_shift.name - for w in workers ) - ) - self.model.constraints.add( - 1 - >= self.model.works[ - worker.id, week, day, constraint_shift.name - ] - + sum( - p2 * self.model.works[w.id, p2week, p2day, shift] - for shift in self.get_shift_names_by_week_day( - p2week, p2day + # self.model.constraints.add( + # 1 >= self.model.works[worker.id, week, day, + # constraint_shift.name] + + # sum(n3 * self.model.works[worker.id, n3week, n3day, + # shift] + # for shift in self.get_shift_names_by_week_day(week, n3day) + # if shift != constraint_shift.name)) + self.model.constraints.add( + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + p1 * self.model.works[w.id, pweek, pday, shift] + for shift in self.get_shift_names_by_week_day( + pweek, pday + ) + if shift != constraint_shift.name + for w in workers + ) + ) + self.model.constraints.add( + 1 + >= self.model.works[ + worker.id, week, day, constraint_shift.name + ] + + sum( + p2 * self.model.works[w.id, p2week, p2day, shift] + for shift in self.get_shift_names_by_week_day( + p2week, p2day + ) + if shift != constraint_shift.name + for w in workers ) - if shift != constraint_shift.name - for w in workers ) - ) self.define_objectives() @@ -1521,14 +1625,27 @@ class RotaBuilder(object): balance_modifier_constant = 1 + if self.constraint_options["balance_shifts_over_workers"]: + worker_shift_balancing = sum( + balance_modifier_constant + * ( + self.model.worker_shift_count_t1[(worker.id)] + + self.model.worker_shift_count_t2[(worker.id)] + ) + for worker in self.workers + ) + else: + worker_shift_balancing = 0 + if self.constraint_options["balance_shifts"]: shift_balancing = sum( balance_modifier_constant * ( - self.model.shift_count_t1[(worker.id)] - + self.model.shift_count_t2[(worker.id)] + self.model.shift_count_t1[worker.id, shift.name] + + self.model.shift_count_t2[worker.id, shift.name] ) for worker in self.workers + for shift in self.get_shifts() ) else: shift_balancing = 0 @@ -1598,7 +1715,6 @@ class RotaBuilder(object): ) # # Spread nights - if self.constraint_options["balance_nights_across_sites"]: nights_site_balancing = sum( ( @@ -1631,14 +1747,10 @@ class RotaBuilder(object): # print(worker_a, worker_b) # for week, day in self.get_week_day_combinations(): # worker_pairs_balancing = worker_pairs_balancing + sum(self.model.works[worker_a.id, week, day, shift] for shift in self.get_shift_names_by_week_day(week, day)) * sum(self.model.works[worker_b.id, week, day, shift] for shift in self.get_shift_names_by_week_day(week, day)) * worker_pairs_constant - - # return weekend_shift_balancing + blocks_balancing - # return bank_holiday_balancing - # return shift_balancing + preferences + blocks_balancing - # return shift_balancing + preferences + nights_site_balancing + blocks_balancing return ( weekend_shift_balancing + shift_balancing + + worker_shift_balancing + night_shift_balancing + shift_diff_balancing + preferences @@ -1712,7 +1824,7 @@ class RotaBuilder(object): """ self.shifts.extend(shifts) - def get_shift_names_by_week_day(self, week, day: DayStr) -> Set(): + def get_shift_names_by_week_day(self, week, day: DayStr) -> set(): """Returns the shifts required for a specific day Returns: @@ -1833,6 +1945,9 @@ class RotaBuilder(object): """ return self.weeks_days_product + def get_week_day_combinations_for_shift(self, shift) -> list: + return [(week, day) for week, day in self.get_week_day_combinations() if shift.name in self.get_shift_names_by_week_day(week, day)] + def get_shifts(self) -> List[SingleShift]: """Returns a list of all the registered shifts @@ -1844,6 +1959,15 @@ class RotaBuilder(object): def get_shifts_with_constraint(self, constraint) -> List[SingleShift]: return [shift for shift in self.shifts if constraint in shift.constraints] + def get_shifts_with_constraints(self, *constraints) -> set[SingleShift]: + shifts = set() + + for constraint in constraints: + shifts.update( + [shift for shift in self.shifts if constraint in shift.constraints] + ) + return shifts + def get_shift_names(self) -> List[ShiftName]: """Returns a list of all the registered shift names @@ -1944,42 +2068,37 @@ class RotaBuilder(object): if self.week_day_date_map[(week, day)] in bank_holiday_map ] - -class RotaResults(object): - def __init__(self, rota): - self.rota = rota - # self.results = results - + # RESULTS def export_rota_to_html(self, filename: str = "rota"): with open(f"{filename}.html", "w") as f: f.write(self.get_worker_timetable_html(True)) def export_rota_to_csv(self, filename: str = "rota"): - works = self.rota.model.works + works = self.model.works with open(f"{filename}.csv", "w", newline="") as f: wr = csv.writer(f, quoting=csv.QUOTE_ALL) l = ["Name"] - l.extend([worker.name for worker in self.rota.workers]) + l.extend([worker.name for worker in self.workers]) wr.writerow(l) l2 = ["Site"] - l2.extend([worker.site for worker in self.rota.workers]) + l2.extend([worker.site for worker in self.workers]) wr.writerow(l2) l3 = ["Grade"] - l3.extend([worker.grade for worker in self.rota.workers]) + l3.extend([worker.grade for worker in self.workers]) wr.writerow(l3) l4 = ["FTE"] - l4.extend([worker.fte for worker in self.rota.workers]) + l4.extend([worker.fte for worker in self.workers]) wr.writerow(l4) - for week, day in self.rota.get_week_day_combinations(): + for week, day in self.get_week_day_combinations(): d = [f"Week {week} Day {day}"] - for worker in self.rota.workers: + for worker in self.workers: i = "" - for shift in self.rota.get_shift_names_by_week_day(week, day): + for shift in self.get_shift_names_by_week_day(week, day): if works[worker.id, week, day, shift].value == 1: i = shift @@ -1988,30 +2107,27 @@ class RotaResults(object): def get_work_table(self): """Build a timetable of the week as a dictionary from the model's optimal solution.""" - works = self.rota.model.works + works = self.model.works week_table = { - week: { - day: {shift: [] for shift in self.rota.get_shift_names()} - for day in days - } - for week in self.rota.weeks + week: {day: {shift: [] for shift in self.get_shift_names()} for day in days} + for week in self.weeks } - for week in self.rota.weeks: - for worker in self.rota.workers: - for day, shift in self.rota.get_day_shiftname_combinations(): + for week in self.weeks: + for worker in self.workers: + for day, shift in self.get_day_shiftname_combinations(): if works[worker.id, week, day, shift].value == 1: week_table[week][day][shift].append(worker.get_details()) return week_table def get_worker_timetable(self): - works = self.rota.model.works + works = self.model.works timetable = { - worker.name: {week: {day: "" for day in days} for week in self.rota.weeks} - for worker in self.rota.workers + worker.name: {week: {day: "" for day in days} for week in self.weeks} + for worker in self.workers } - for worker in self.rota.workers: - for week in self.rota.weeks: - for day, shift in self.rota.get_day_shiftname_combinations(): + for worker in self.workers: + for week in self.weeks: + for day, shift in self.get_day_shiftname_combinations(): if works[worker.id, week, day, shift].value == 1: timetable[worker.name][week][day] = shift return timetable @@ -2019,18 +2135,18 @@ class RotaResults(object): def get_worker_timetable_brief( self, show_prefs=False, marker_every=30, show_unavailable=False ): - model = self.rota.model + model = self.model week_string = f"{'-Week-':20}" + "".join( - [7 * str(f"{w}")[-1:] for w in self.rota.weeks] + [7 * str(f"{w}")[-1:] for w in self.weeks] ) - days_string = f"{'-Day-':20}" + "".join("MTWTFSS" * len(self.rota.weeks)) + days_string = f"{'-Day-':20}" + "".join("MTWTFSS" * len(self.weeks)) timetable = [] - for worker in self.rota.workers: + for worker in self.workers: shifts = [] w = [f"{worker.name:20}"] - for week, day in self.rota.get_week_day_combinations(): + for week, day in self.get_week_day_combinations(): a = "-" - for shift in self.rota.get_shift_names_by_week_day(week, day): + for shift in self.get_shift_names_by_week_day(week, day): if model.works[worker.id, week, day, shift].value > 0: shifts.append(shift) a = shift[0] @@ -2049,7 +2165,7 @@ class RotaResults(object): if show_prefs: # prefs w = [f"{'Preferences':20}"] - for week, day in self.rota.get_week_day_combinations(): + for week, day in self.get_week_day_combinations(): if model.pref_not_to_work[worker.id, week, day] > 0: w.append("Y") else: @@ -2059,7 +2175,7 @@ class RotaResults(object): if show_unavailable: # prefs w = [f"{'Unavailable':20}"] - for week, day in self.rota.get_week_day_combinations(): + for week, day in self.get_week_day_combinations(): if model.available[worker.id, week, day] > 0: w.append("A") else: @@ -2080,30 +2196,30 @@ class RotaResults(object): def get_worker_timetable_html( self, include_html_tag=False, table_name="rota-table" ): - model = self.rota.model + model = self.model timetable = [] timetable.append( - f"
- {json.dumps(self.rota.constraint_options, indent=4)}
+ {json.dumps(self.constraint_options, indent=4)}