from copy import deepcopy from re import S from black import main import pytest from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days import datetime from rota.workers import Worker class TestDemoRota: # Set up rota weeks_to_rota = 10 start_date = datetime.date(2022, 3, 7) Rota = RotaBuilder( start_date, weeks_to_rota=weeks_to_rota, balance_offset_modifier=1, ) Rota.constraint_options["max_shifts_per_week"] = 5 Rota.constraint_options["max_shifts_per_month"] = 31 Rota.constraint_options["constrain_time_off_after_nights"] = False Rota.constraint_options["balance_nights_across_site"] = False Rota.constraint_options["balance_shifts_quadratic"] = True Rota.constraint_options["balance_shifts_over_workers"] = True Rota.constraint_options["balance_nights"] = False Rota.constraint_options["minimise_shift_diffs"] = False Rota.constraint_options["balance_blocks"] = False Rota.constraint_options["balance_bank_holidays"] = False # Add a few workers worker1 = Worker(name="worker1", site="group1", grade=1) worker2 = Worker(name="worker2", site="group1", grade=1) worker3 = Worker(name="worker3", site="group1", grade=1) worker4 = Worker(name="worker4", site="group1", grade=1) worker5 = Worker(name="worker5", site="group2", grade=1, fte=60) worker6 = Worker(name="worker6", site="group2", grade=1, fte=40) Rota.add_workers((worker1, worker2, worker3, worker4, worker5, worker6)) # Add a weekday and weekend shift Rota.add_shifts( SingleShift( sites=("group1", "group2"), name="weekday", length=12.5, days=days[:5], balance_offset=1, workers_required=1, # We use a different balance weighting for each shift as otherwise # the shifts can be treated equally (and no target diff) # balance_weighting=0.6, ), SingleShift( sites=("group1",), name="weekend", length=12.5, days=days[5:], balance_offset=4, # balance_weighting=0.5 ), ) Rota.build_shifts() Rota.build_workers() Rota.build_model() print(Rota.get_worker_details()) for w in Rota.get_workers(): print(w) print(w.get_shift_targets()) solver_options = {"ratio": 0, "seconds": 1000, "threads": 10} Rota.solve_model(options=solver_options) # print(Rota.get_worker_details()) # optimizer = SolverFactory('cbc') # result = optimizer.solve(prob,tee=True) # result.Solver.Status = SolverStatus.warning # prob.solutions.load_from(result) # worker_timetable_brief = Rota.get_worker_timetable_brief( # show_prefs=False, show_unavailable=False # ) # print(worker_timetable_brief) # print(Rota.get_shift_summary_dict()) Rota.export_rota_to_html("test2") def test_optimal_solution(self): assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_start_date(self): assert self.Rota.start_date == self.start_date def test_end_date(self): assert self.Rota.rota_end_date == self.start_date + datetime.timedelta( weeks=self.weeks_to_rota ) def test_rota_workers_number(self): assert len(self.Rota.workers) == 6 def test_rota_workers_total_fte(self): assert self.Rota.get_workers_total_fte() == 500 def test_weekday_workers(self): weekday_shift_workers = self.Rota.get_workers_for_shift( self.Rota.get_shift_by_name("weekday") ) for worker in weekday_shift_workers: assert worker.proportion_rota_to_work == 1 assert len(weekday_shift_workers) == 6 def test_weekend_workers(self): weekend_shift_workers = self.Rota.get_workers_for_shift( self.Rota.get_shift_by_name("weekend") ) for worker in weekend_shift_workers: assert worker.proportion_rota_to_work == 1 # assert worker.get_shift_targets() == {} assert len(weekend_shift_workers) == 4 def test_worker_shift_targets(self): for worker in self.Rota.workers: match worker.site: case "group1": assert worker.proportion_rota_to_work == 1 assert worker.get_shift_targets() == {"weekday": 10, "weekend": 5} case "group2": assert worker.proportion_rota_to_work == 1 match worker.fte: case 60: assert worker.get_shift_targets() == { "weekday": 6, "weekend": 0, } case 40: assert worker.get_shift_targets() == { "weekday": 4, "weekend": 0, } case _: assert 1 == 0 case _: assert 1 == 0 def test_worker_shift_targets2(self): shift_number = self.Rota.get_shift_summary_dict() for worker in self.Rota.workers: match worker.site: case "group1": assert shift_number[worker.name]["weekday"] == 10 assert shift_number[worker.name]["weekend"] == 5 case "group2": assert shift_number[worker.name]["weekend"] == 0 match worker.fte: case 60: assert shift_number[worker.name]["weekday"] == 6 case 40: assert shift_number[worker.name]["weekday"] == 4 case _: assert 1 == 0 case _: assert 1 == 0 def test_rota_sites(self): assert self.Rota.sites == {"group1", "group2"} class TestDemoRotaNights: # Set up rota weeks_to_rota = 12 start_date = datetime.date(2022, 3, 14) Rota = RotaBuilder( start_date, weeks_to_rota=weeks_to_rota, balance_offset_modifier=1, ) Rota.constraint_options["max_shifts_per_week"] = 5 Rota.constraint_options["max_shifts_per_month"] = 31 Rota.constraint_options["limit_to_1_st2_on_nights"] = False Rota.constraint_options["constrain_time_off_after_nights"] = False Rota.constraint_options["balance_nights_across_site"] = False Rota.constraint_options["balance_shifts"] = True Rota.constraint_options["balance_shifts_over_workers"] = True Rota.constraint_options["balance_nights"] = False Rota.constraint_options["minimise_shift_diffs"] = False Rota.constraint_options["balance_blocks"] = False # Rota.constraint_options["balance_weekends"] = False # Add a few workers worker1 = Worker(name="worker1", site="group1", grade=1) worker2 = Worker(name="worker2", site="group1", grade=1) worker3 = Worker(name="worker3", site="group1", grade=1) worker4 = Worker(name="worker4", site="group1", grade=1) worker5 = Worker(name="worker5", site="group2", grade=1, fte=60) worker6 = Worker(name="worker6", site="group2", grade=1, fte=40) worker7 = Worker(name="worker7", site="group2", grade=1) worker8 = Worker(name="worker8", site="group2", grade=1) Rota.add_workers( (worker1, worker2, worker3, worker4, worker5, worker6, worker7, worker8) ) # Add a weekday and weekend shift Rota.add_shifts( SingleShift( sites=("group1", "group2"), name="night_weekday", length=12.5, days=days[:4], workers_required=1, constraint=[{"name": "preclear2"}, {"name": "postclear2"}], # constraint=["night"], force_as_block=True # We use a different balance weighting for each shift as otherwise # the shifts can be treated equally (and no target diff) # balance_weighting=0.6, ), SingleShift( sites=("group1", "group2"), name="night_weekend", length=12.5, days=days[4:], # balance_weighting=0.5 constraint=[{"name": "preclear2"}, {"name": "postclear2"}], # constraint=["night"], force_as_block=True, ), SingleShift( sites=("group1", "group2"), name="twilight", length=12.5, days=days[:5], # balance_weighting=0.5 workers_required=2, # constraint=["preclear2", "postclear2"], ), # SingleShift(("group1","group2"), "atwilight", 12.5, days[:5], balance_offset=40, ##balance_weighting=0.5 # workers_required=2, # #constraint=["postclear2"], # ), ) Rota.build_shifts() Rota.build_workers() Rota.build_model() solver_options = {"ratio": 0.00, "seconds": 1000, "threads": 10} Rota.solve_model(options=solver_options) Rota.export_rota_to_html("test3") def test_start_date(self): assert self.Rota.start_date == self.start_date def test_night_assignment(self): shift_summary = self.Rota.get_shift_summary_dict() for worker_name in shift_summary: worker_shifts = shift_summary[worker_name] worker = self.Rota.get_worker_by_name(worker_name) if worker.fte == 40: assert worker_shifts["night_weekday"] in (0, 4) assert worker_shifts["night_weekend"] in (0, 3) elif worker.fte == 60: assert worker_shifts["night_weekday"] in (0, 4, 8) assert worker_shifts["night_weekend"] in (0, 3, 6) else: assert worker_shifts["night_weekday"] in (4, 8) assert worker_shifts["night_weekend"] in (3, 6) class TestDemoRotaClear: weeks_to_rota = 10 start_date = datetime.date(2022, 3, 7) Rota = RotaBuilder( start_date, weeks_to_rota=weeks_to_rota, balance_offset_modifier=1, ) Rota.constraint_options["max_shifts_per_month"] = 20 worker1 = Worker(name="worker1", site="group1", grade=1) worker2 = Worker(name="worker2", site="group1", grade=1) worker3 = Worker(name="worker3", site="group1", grade=1) worker4 = Worker(name="worker4", site="group1", grade=1) worker5 = Worker(name="worker5", site="group2", grade=1, fte=60) worker6 = Worker(name="worker6", site="group2", grade=1, fte=40) worker7 = Worker(name="worker7", site="group2", grade=1) worker8 = Worker(name="worker8", site="group2", grade=1) Rota.add_workers( (worker1, worker2, worker3, worker4, worker5, worker6, worker7, worker8) ) def test_preclear(self): self.Rota.add_shifts( SingleShift( sites=("group1", "group2"), name="a", length=12.5, days=days[:5], balance_offset=40, workers_required=2, constraint=[{"name": "preclear"}], ), SingleShift( sites=("group1", "group2"), name="b", length=12.5, days=days[:5], balance_offset=40, workers_required=2, ), SingleShift( sites=("group1", "group2"), name="c", length=12.5, days=days[3], balance_offset=40, workers_required=1, constraint=[{"name": "preclear2"}, {"name": "postclear2"}], ), ) self.Rota.build_shifts() self.Rota.build_workers() self.Rota.build_model() solver_options = {"ratio": 0.1, "seconds": 1000, "threads": 10} self.Rota.solve_model(options=solver_options) self.Rota.export_rota_to_html("test4") assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" for worker in self.Rota.workers: shifts = self.Rota.get_worker_shift_list(worker) # Convert shift to a string representation shifts_string = "".join([i if i != "" else "-" for i in shifts]) # check for patterns that should not occur assert "ba" not in shifts_string assert "ca" not in shifts_string assert "ac" not in shifts_string assert "ca" not in shifts_string assert "a-c" not in shifts_string assert "c-a" not in shifts_string assert "bc" not in shifts_string assert "cb" not in shifts_string assert "b-c" not in shifts_string assert "c-b" not in shifts_string class TestDemoRotaShiftConstraints: weeks_to_rota = 10 start_date = datetime.date(2022, 3, 7) Rota = RotaBuilder( start_date, weeks_to_rota=weeks_to_rota, balance_offset_modifier=1, ) # Rota.constraint_options["max_shifts_per_week"] = 5 # Rota.constraint_options["max_shifts_per_month"] = 20 worker1 = Worker(name="worker1", site="group1", grade=1) worker2 = Worker(name="worker2", site="group1", grade=1) Rota.add_workers((worker1, worker2)) Rota.add_shifts( SingleShift( sites=("group1",), name="a", length=12.5, days=days, balance_offset=40, workers_required=1, ), ) Rota3Workers = deepcopy(Rota) worker3 = Worker(name="worker3", site="group1", grade=1) Rota3Workers.add_worker(worker3) def test_max_shifts(self): self.Rota.constraint_options["max_shifts_per_month"] = 14 self.Rota.build_and_solve() assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_max_shifts_fail(self): self.Rota.constraint_options["max_shifts_per_month"] = 13 self.Rota.build_and_solve() assert self.Rota.results.solver.status == "warning" assert self.Rota.results.solver.termination_condition == "infeasible" def test_max_shifts_extra_worker(self): self.Rota3Workers.constraint_options["max_shifts_per_month"] = 12 self.Rota3Workers.build_and_solve() assert self.Rota3Workers.results.solver.status == "ok" assert self.Rota3Workers.results.solver.termination_condition == "optimal" def test_max_shifts_per_week_fail(self): self.Rota.constraint_options["max_shifts_per_week"] = 3 self.Rota.build_and_solve() assert self.Rota.results.solver.status == "warning" assert self.Rota.results.solver.termination_condition == "infeasible" def test_max_shifts_per_week_pass(self): self.Rota.constraint_options["max_shifts_per_month"] = 14 self.Rota.constraint_options["max_shifts_per_week"] = 4 self.Rota.build_and_solve() assert self.Rota.results.solver.status == "ok" def test_max_shifts_per_week_extra_worker_pass(self): self.Rota3Workers.constraint_options["max_shifts_per_week"] = 3 # worker3 = Worker(name="worker3", site="group1", grade=1) # self.Rota.add_worker(worker3) self.Rota3Workers.build_and_solve() assert self.Rota3Workers.results.solver.status == "ok" class TestDemoRotaBalanceShiftSites: weeks_to_rota = 8 start_date = datetime.date(2022, 3, 7) Rota = RotaBuilder( start_date, weeks_to_rota=weeks_to_rota, ) # Rota.constraint_options["max_shifts_per_week"] = 5 # Rota.constraint_options["max_shifts_per_month"] = 20 worker1 = Worker(name="worker1", site="group1", grade=1) worker2 = Worker(name="worker2", site="group1", grade=1) worker3 = Worker(name="worker3", site="group2", grade=1) worker4 = Worker(name="worker4", site="group2", grade=1) worker5 = Worker(name="worker5", site="group3", grade=1) worker6 = Worker(name="worker6", site="group3", grade=1) Rota.add_workers((worker1, worker2, worker3, worker4)) Rota.add_workers((worker5, worker6)) def test_balance_blocks_across_2_groups(self): self.Rota.add_shifts( SingleShift( sites=("group1", "group2"), name="weekday_night", length=12.5, days=days[:4], balance_offset=40, workers_required=2, force_as_block=True, constraint=[{"name": "balance_across_groups"}], ), ) self.Rota.constraint_options["balance_nights_across_sites"] = False self.Rota.constraint_options["balance_nights"] = False self.Rota.constraint_options["constrain_time_off_after_nights"] = False self.Rota.build_and_solve(options={"ratio": 0.0}) self.Rota.export_rota_to_html("test5") print(self.Rota.get_workers_by_group()) group_workers = self.Rota.get_workers_by_group() for group in group_workers: shift_patterns = [] for w in group_workers[group]: shift_patterns.append(self.Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") <= 1 assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_balance_blocks_across_2_groups_unbalanced(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( sites=("group1", "group2"), name="weekday_night", length=12.5, days=days[:4], balance_offset=40, workers_required=3, force_as_block=True, constraint=[{"name": "balance_across_groups"}], ), ) self.Rota.constraint_options["balance_nights_across_sites"] = False self.Rota.constraint_options["balance_nights"] = False self.Rota.constraint_options["constrain_time_off_after_nights"] = False self.Rota.build_and_solve(options={"ratio": 0.1}) group_workers = self.Rota.get_workers_by_group() for group in group_workers: shift_patterns = [] for w in group_workers[group]: shift_patterns.append(self.Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") <= 2 assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_balance_blocks_across_3_groups(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days[:4], balance_offset=40, workers_required=3, force_as_block=True, constraint=[{"name": "balance_across_groups"}], ), ) self.Rota.constraint_options["balance_nights_across_sites"] = False self.Rota.constraint_options["balance_nights"] = False self.Rota.constraint_options["constrain_time_off_after_nights"] = False self.Rota.build_and_solve(options={"ratio": 0.1}) print("HELLO") group_workers = self.Rota.get_workers_by_group() for group in group_workers: shift_patterns = [] for w in group_workers[group]: shift_patterns.append(self.Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") <= 1 assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def generate_limit_constraint_rota(): weeks_to_rota = 8 start_date = datetime.date(2022, 3, 7) Rota = RotaBuilder( start_date, weeks_to_rota=weeks_to_rota, ) # Rota.constraint_options["max_shifts_per_week"] = 5 # Rota.constraint_options["max_shifts_per_month"] = 20 worker1 = Worker(name="worker1", site="group1", grade=2, remote_site="group1") worker2 = Worker(name="worker2", site="group1", grade=2, remote_site="group1") worker3 = Worker(name="worker3", site="group1", grade=2, remote_site="group1") worker4 = Worker(name="worker4", site="group3", grade=3, remote_site="group2") worker5 = Worker(name="worker5", site="group3", grade=3, remote_site="group2") worker6 = Worker(name="worker6", site="group3", grade=3, remote_site="group2") Rota.add_workers((worker1, worker2, worker3, worker4)) Rota.add_workers((worker5, worker6)) Rota.shifts = [] return Rota class TestLimitConstraints: def test_constraint_limit_grades(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=60, workers_required=4, force_as_block=True, constraint=[{"name": "limit_grade_number", "options": {2: 1}}], ), ) # Rota.constraint_options["balance_nights_across_sites"] = False # Rota.constraint_options["balance_shifts_over_workers"] = False #Rota.constraint_options["balance_shifts_quadratic"] = True Rota.build_and_solve(options={"ratio": 0.1}) Rota.export_rota_to_html("constraint_limit_grades") assert Rota.results.solver.status == "ok" assert Rota.results.solver.termination_condition == "optimal" grade_workers = Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) limit = 1 if grade > 2: limit = 3 for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") == limit def test_constraint_limit_grades2(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[{"name": "limit_grade_number", "options": {3: 2}}], ), ) # Rota.constraint_options["balance_nights_across_sites"] = False # Rota.constraint_options["balance_shifts_over_workers"] = False Rota.constraint_options["balance_shifts_quadratic"] = True Rota.build_and_solve(options={"ratio": 0.1}) Rota.export_rota_to_html("test9") grade_workers = Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) limit = 2 if grade > 2: limit = 2 for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") == limit assert Rota.results.solver.status == "ok" assert Rota.results.solver.termination_condition == "optimal" def test_constraint_limit_grades3(self): Rota = generate_limit_constraint_rota() Rota.workers = [] worker1 = Worker(name="worker1", site="group1", grade=2, remote_site="group1") worker2 = Worker(name="worker2", site="group1", grade=2, remote_site="group1") worker3 = Worker(name="worker3", site="group1", grade=2, remote_site="group1") worker4 = Worker(name="worker4", site="group3", grade=3, remote_site="group2") worker5 = Worker(name="worker5", site="group3", grade=3, remote_site="group2") worker6 = Worker(name="worker6", site="group3", grade=3, remote_site="group2") worker7 = Worker(name="worker7", site="group3", grade=2, remote_site="group2") Rota.add_workers((worker1, worker2, worker3, worker4)) Rota.add_workers((worker5, worker6)) Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=60, workers_required=4, force_as_block=True, constraint=[{"name": "limit_grade_number", "options": {2: 3, 3: 1}},] ), ) # Rota.constraint_options["balance_nights_across_sites"] = False # Rota.constraint_options["balance_shifts_over_workers"] = False Rota.constraint_options["balance_shifts_quadratic"] = True Rota.build_and_solve(options={"ratio": 0.01}) Rota.export_rota_to_html("test9") assert Rota.results.solver.status == "ok" assert Rota.results.solver.termination_condition == "optimal" grade_workers = Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) limit = 3 if grade > 2: limit = 1 for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") == limit def test_constraint_limit_grades4(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[{"name": "limit_grade_number", "options": {2: 4, 3: 0}}], ), ) # Rota.constraint_options["balance_nights_across_sites"] = False # Rota.constraint_options["balance_shifts_over_workers"] = False Rota.constraint_options["balance_shifts_quadratic"] = True Rota.build_and_solve(options={"ratio": 0.1}) assert Rota.results.solver.termination_condition == "infeasible" def test_constraint_minimum_grades(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=40, workers_required=1, force_as_block=True, constraint=[{"name": "minimum_grade_number", "options": (3, 1)}], ), ) Rota.constraint_options["balance_shifts_quadratic"] = True Rota.build_and_solve(options={"ratio": 0.1}) Rota.export_rota_to_html("test9") assert Rota.results.solver.status == "ok" grade_workers = Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) limit = 0 if grade > 2: limit = 1 for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") == limit def test_constraint_minimum_grades2(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[{"name": "minimum_grade_number", "options": (3, 3)}], ), ) Rota.constraint_options["balance_shifts_quadratic"] = True Rota.build_and_solve(options={"ratio": 0.1}) Rota.export_rota_to_html("test9") grade_workers = Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) limit = 1 if grade > 2: limit = 3 for day_shifts in zipped_lists: assert day_shifts.count("weekday_night") == limit assert Rota.results.solver.status == "ok" def test_constraint_minimum_grades3(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[{"name": "minimum_grade_number", "options": (3, 0)}], ), ) Rota.constraint_options["balance_shifts_quadratic"] = True Rota.build_and_solve(options={"ratio": 0.1}) Rota.export_rota_to_html("test9") assert Rota.results.solver.status == "ok" def test_constraint_minimum_grades_no_valid_worker(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="weekday_night", length=12.5, days=days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[{"name": "minimum_grade_number", "options": (4, 1)}], ), ) Rota.constraint_options["balance_shifts_quadratic"] = True with pytest.raises(ValueError): Rota.build_and_solve(options={"ratio": 0.1}) def test_constraint_require_remote_site_presence_week(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="night", length=12.5, days=days, balance_offset=40, workers_required=2, force_as_block=True, constraint=[ { "name": "require_remote_site_presence_week", "options": ("group1", 2), } ], ), ) Rota.build_and_solve(options={"ratio": 0.00}) Rota.export_rota_to_html("remote1") group_workers = Rota.get_workers_by_group() for group in group_workers: shift_patterns = [] for w in group_workers[group]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) limit = 2 if group != "group1": limit = 0 for day_shifts in zipped_lists: assert day_shifts.count("night") == limit assert Rota.results.solver.status == "ok" assert Rota.results.solver.termination_condition == "optimal" def test_constraint_require_remote_site_presence_week2(self): Rota = generate_limit_constraint_rota() Rota.constraint_options["minimise_shift_diffs"] = False Rota.constraint_options["balance_shifts_quadratic"] = False Rota.constraint_options["balance_shift"] = False Rota.constraint_options["balance_nights_across_sites"] = True Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="night", length=12.5, days=days, balance_offset=20, workers_required=3, force_as_block=True, constraint=[ { "name": "require_remote_site_presence_week", "options": ("group1", 2), } ], ), ) Rota.build_and_solve(options={"ratio": 0.00}) Rota.export_rota_to_html("remote2") group_workers = Rota.get_workers_by_remote_group() for group in group_workers: shift_patterns = [] for w in group_workers[group]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) if group == "group1": for day_shifts in zipped_lists: assert day_shifts.count("night") >= 2 assert Rota.results.solver.status == "ok" assert Rota.results.solver.termination_condition == "optimal" def test_constraint_require_remote_site_presence_week3(self): Rota = generate_limit_constraint_rota() Rota.shifts = [] Rota.add_shifts( SingleShift( sites=("group1", "group2", "group3"), name="night", length=12.5, days=days, balance_offset=20, workers_required=3, force_as_block=True, constraint=[ { "name": "require_remote_site_presence_week", "options": ("group2", 1), } ], ), ) Rota.build_and_solve(options={"ratio": 0.00}) Rota.export_rota_to_html("remote3") group_workers = Rota.get_workers_by_remote_group() for group in group_workers: shift_patterns = [] for w in group_workers[group]: shift_patterns.append(Rota.get_worker_shift_list(w)) zipped_lists = list(zip(*shift_patterns)) if group == "group2": for day_shifts in zipped_lists: assert day_shifts.count("night") >= 1 assert Rota.results.solver.status == "ok" assert Rota.results.solver.termination_condition == "optimal" # class TestNoWorkerRota: # weeks_to_rota = 10 # start_date = datetime.date(2022, 3, 7) # # Rota = RotaBuilder( # start_date, # weeks_to_rota=weeks_to_rota, # ) # # def test_no_workers(self): # with pytest.raises(NoWorkers): # self.Rota.build_workers() class TestNightUnavailable: # Set up rota weeks_to_rota = 10 start_date = datetime.date(2022, 3, 7) Rota = RotaBuilder( start_date, weeks_to_rota=weeks_to_rota, ) # Rota.constraint_options["balance_weekends"] = False # Add a few workers worker1 = Worker( name="worker1", site="group1", grade=1, not_available_to_work=[ { "date": datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(), "reason": "****", }, ], ) worker2 = Worker( name="worker2", site="group1", grade=1, not_available_to_work=[ { "date": datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(), "reason": "****", } ], ) Rota.add_workers((worker1, worker2)) def test_basic_assignment(self, capsys): self.Rota.shifts = [] # Add a weekday and weekend shift self.Rota.add_shifts( SingleShift( sites =("group1",), name = "night_weekend", length = 12.5, days = days[5:], constraint=[{"name": "night"}], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("testnight") assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" self.Rota.export_rota_to_html("testnight") summary = self.Rota.get_shift_summary_dict() for worker in summary: assert summary[worker]["night_weekend"] == 10 def test_assign_night_prior_to_unavailablity(self): self.Rota.shifts = [] # Add a weekday and weekend shift self.Rota.add_shifts( SingleShift( sites =("group1",), name = "night_weekend", length = 12.5, days = days[5:], workers_required=2, constraint=[{"name": "night"}], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) assert self.Rota.results.solver.status == "warning" assert self.Rota.results.solver.termination_condition == "infeasible" def test_assign_prior_to_unavailablity_non_night(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( sites = ("group1",), name = "night_weekend", length = 12.5, days = days[5:], workers_required=2, constraint=[], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("testnight") assert self.Rota.results.solver.status == "ok" def test_assign_split(self): self.Rota.shifts = [] worker3 = Worker( name="worker3", site="group1", grade=1, not_available_to_work=( {"date": datetime.datetime.strptime("13/03/22", "%d/%m/%y").date()}, ), ) worker4 = Worker( name="worker4", site="group1", grade=1, not_available_to_work=( {"date": datetime.datetime.strptime("12/03/22", "%d/%m/%y").date()}, ), ) self.Rota.add_workers((worker3, worker4)) self.Rota.add_shifts( SingleShift( sites=("group1",), name="night_weekend", length=12.5, days=days[5:], workers_required=3, constraint=[], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("testnight") assert self.Rota.results.solver.status == "ok" summary = self.Rota.get_shift_summary_dict() for worker in summary: assert summary[worker]["night_weekend"] == 15 # def test_assign_split_night_constraint(self): # self.Rota.shifts = [] # worker3 = Worker( # self.Rota, name="worker3", site="group1", grade=1, not_available_to_work=((datetime.datetime.strptime("13/03/22", "%d/%m/%y").date(), "****"),) # ) # worker4 = Worker( # self.Rota, name="worker4", site="group1", grade=1, not_available_to_work=((datetime.datetime.strptime("12/03/22", "%d/%m/%y").date(), "****"),) # ) # self.Rota.add_workers((worker3, worker4)) # # self.Rota.add_shifts( # SingleShift( # ("group1",), "night_weekend", 12.5, days[5:], workers_required=3, constraint=["night"] # ), # ) # # self.Rota.build_and_solve(options={"ratio": 0.00}) # # assert self.Rota.results.solver.status == "warning" if __name__ == "__main__": t = TestLimitConstraints() t.test_constraint_require_remote_site_presence_week()