from copy import deepcopy 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, max_weekend_frequency=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( ("group1", "group2"), "weekday", 12.5, 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( ("group1",), "weekend", 12.5, 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, max_weekend_frequency=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( ("group1", "group2"), "night_weekday", 12.5, days[:4], workers_required=1, constraint=["preclear2", "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( ("group1", "group2"), "night_weekend", 12.5, days[4:], # balance_weighting=0.5 constraint=["preclear2", "postclear2"], # constraint=["night"], force_as_block=True, ), SingleShift( ("group1", "group2"), "twilight", 12.5, 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, max_weekend_frequency=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( ("group1", "group2"), "a", 12.5, days[:5], balance_offset=40, workers_required=2, constraint=["preclear"], ), SingleShift( ("group1", "group2"), "b", 12.5, days[:5], balance_offset=40, workers_required=2, ), SingleShift( ("group1", "group2"), "c", 12.5, days[3], balance_offset=40, workers_required=1, constraint=["preclear2", "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, max_weekend_frequency=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( ("group1",), "a", 12.5, 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( ("group1", "group2"), "weekday_night", 12.5, days[:4], balance_offset=40, workers_required=2, force_as_block=True, constraint=["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( ("group1", "group2"), "weekday_night", 12.5, days[:4], balance_offset=40, workers_required=3, force_as_block=True, constraint=["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( ("group1", "group2", "group3"), "weekday_night", 12.5, days[:4], balance_offset=40, workers_required=3, force_as_block=True, constraint=["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" class TestLimitConstraints: 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 = [] def test_constraint_limit_grades(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[("limit_grade_number", {2: 1})], ), ) # self.Rota.constraint_options["balance_nights_across_sites"] = False # self.Rota.constraint_options["balance_shifts_over_workers"] = False self.Rota.constraint_options["balance_shifts_quadratic"] = True self.Rota.build_and_solve(options={"ratio": 0.00}) grade_workers = self.Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(self.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 self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_constraint_limit_grades2(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[("limit_grade_number", {3: 2})], ), ) # self.Rota.constraint_options["balance_nights_across_sites"] = False # self.Rota.constraint_options["balance_shifts_over_workers"] = False self.Rota.constraint_options["balance_shifts_quadratic"] = True self.Rota.build_and_solve(options={"ratio": 0.1}) self.Rota.export_rota_to_html("test9") grade_workers = self.Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(self.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 self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_constraint_limit_grades3(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[("limit_grade_number", {2: 3, 3: 1})], ), ) # self.Rota.constraint_options["balance_nights_across_sites"] = False # self.Rota.constraint_options["balance_shifts_over_workers"] = False self.Rota.constraint_options["balance_shifts_quadratic"] = True self.Rota.build_and_solve(options={"ratio": 0.1}) self.Rota.export_rota_to_html("test9") grade_workers = self.Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(self.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 assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_constraint_limit_grades4(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[("limit_grade_number", {2: 4, 3: 0})], ), ) # self.Rota.constraint_options["balance_nights_across_sites"] = False # self.Rota.constraint_options["balance_shifts_over_workers"] = False self.Rota.constraint_options["balance_shifts_quadratic"] = True self.Rota.build_and_solve(options={"ratio": 0.1}) assert self.Rota.results.solver.termination_condition == "infeasible" def test_constraint_minimum_grades(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=1, force_as_block=True, constraint=[("minimum_grade_number", (3, 1))], ), ) self.Rota.constraint_options["balance_shifts_quadratic"] = True self.Rota.build_and_solve(options={"ratio": 0.1}) self.Rota.export_rota_to_html("test9") grade_workers = self.Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(self.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 assert self.Rota.results.solver.status == "ok" def test_constraint_minimum_grades2(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[("minimum_grade_number", (3, 3))], ), ) self.Rota.constraint_options["balance_shifts_quadratic"] = True self.Rota.build_and_solve(options={"ratio": 0.1}) self.Rota.export_rota_to_html("test9") grade_workers = self.Rota.get_workers_by_grade() for grade in grade_workers: shift_patterns = [] for w in grade_workers[grade]: shift_patterns.append(self.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 self.Rota.results.solver.status == "ok" def test_constraint_minimum_grades3(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[("minimum_grade_number", (3, 0))], ), ) self.Rota.constraint_options["balance_shifts_quadratic"] = True self.Rota.build_and_solve(options={"ratio": 0.1}) self.Rota.export_rota_to_html("test9") assert self.Rota.results.solver.status == "ok" def test_constraint_minimum_grades_no_valid_worker(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=4, force_as_block=True, constraint=[("minimum_grade_number", (4, 1))], ), ) self.Rota.constraint_options["balance_shifts_quadratic"] = True with pytest.raises(ValueError): self.Rota.build_and_solve(options={"ratio": 0.1}) def test_constraint_require_remote_site_presence_week(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "night", 12.5, days, balance_offset=40, workers_required=2, force_as_block=True, constraint=[("require_remote_site_presence_week", ("group1", 2))], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("remote1") 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)) limit = 2 if group != "group1": limit = 0 for day_shifts in zipped_lists: assert day_shifts.count("night") == limit assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_constraint_require_remote_site_presence_week2(self): self.Rota.constraint_options["minimise_shift_diffs"] = False self.Rota.constraint_options["balance_shifts_quadratic"] = False self.Rota.constraint_options["balance_shift"] = False self.Rota.constraint_options["balance_nights_across_sites"] = True self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "night", 12.5, days, balance_offset=20, workers_required=3, force_as_block=True, constraint=[("require_remote_site_presence_week", ("group2", 2))], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("remote2") group_workers = self.Rota.get_workers_by_remote_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)) if group == "group2": for day_shifts in zipped_lists: assert day_shifts.count("night") >= 2 assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_constraint_require_remote_site_presence_week3(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "night", 12.5, days, balance_offset=20, workers_required=3, force_as_block=True, constraint=[("require_remote_site_presence_week", ("group2", 1))], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("remote3") group_workers = self.Rota.get_workers_by_remote_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)) if group == "group2": for day_shifts in zipped_lists: assert day_shifts.count("night") >= 1 assert self.Rota.results.solver.status == "ok" assert self.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( ("group1",), "night_weekend", 12.5, days[5:], constraint=["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( ("group1",), "night_weekend", 12.5, days[5:], workers_required=2, constraint=["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( ("group1",), "night_weekend", 12.5, 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( ("group1",), "night_weekend", 12.5, 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()