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["ensure_derriford_reg_for_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(Rota, "worker1", "group1", 1) worker2 = Worker(Rota, "worker2", "group1", 1) worker3 = Worker(Rota, "worker3", "group1", 1) worker4 = Worker(Rota, "worker4", "group1", 1) worker5 = Worker(Rota, "worker5", "group2", 1, fte=60) worker6 = Worker(Rota, "worker6", "group2", 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=10, 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=40, # 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_targets(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["ensure_derriford_reg_for_nights"] = False 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(Rota, "worker1", "group1", 1) worker2 = Worker(Rota, "worker2", "group1", 1) worker3 = Worker(Rota, "worker3", "group1", 1) worker4 = Worker(Rota, "worker4", "group1", 1) worker5 = Worker(Rota, "worker5", "group2", 1, fte=60) worker6 = Worker(Rota, "worker6", "group2", 1, fte=40) worker7 = Worker(Rota, "worker7", "group2", 1) worker8 = Worker(Rota, "worker8", "group2", 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], balance_offset=10, workers_required=1, constraints=["preclear2", "postclear2"], # constraints=["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_offset=40, # balance_weighting=0.5 constraints=["preclear2", "postclear2"], # constraints=["night"], force_as_block=True, ), SingleShift( ("group1", "group2"), "twilight", 12.5, days[:5], balance_offset=40, # balance_weighting=0.5 workers_required=2, # constraints=["preclear2", "postclear2"], ), # SingleShift(("group1","group2"), "atwilight", 12.5, days[:5], balance_offset=40, ##balance_weighting=0.5 # workers_required=2, # #constraints=["postclear2"], # ), ) 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.1, "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("test3") print(Rota.get_worker_shift_list(worker1)) def test_start_date(self): assert self.Rota.start_date == self.start_date 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(Rota, "worker1", "group1", 1) worker2 = Worker(Rota, "worker2", "group1", 1) worker3 = Worker(Rota, "worker3", "group1", 1) worker4 = Worker(Rota, "worker4", "group1", 1) worker5 = Worker(Rota, "worker5", "group2", 1, fte=60) worker6 = Worker(Rota, "worker6", "group2", 1, fte=40) worker7 = Worker(Rota, "worker7", "group2", 1) worker8 = Worker(Rota, "worker8", "group2", 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, constraints=["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, constraints=["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(Rota, "worker1", "group1", 1) worker2 = Worker(Rota, "worker2", "group1", 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(Rota3Workers, "worker3", "group1", 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(self.Rota, "worker3", "group1", 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(Rota, "worker1", "group1", 1) worker2 = Worker(Rota, "worker2", "group1", 1) worker3 = Worker(Rota, "worker3", "group2", 1) worker4 = Worker(Rota, "worker4", "group2", 1) worker5 = Worker(Rota, "worker5", "group3", 1) worker6 = Worker(Rota, "worker6", "group3", 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, constraints=["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, constraints=["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, constraints=["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") <= 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(Rota, "worker1", "group1", 2, home_site="group1") worker2 = Worker(Rota, "worker2", "group1", 2, home_site="group1") worker3 = Worker(Rota, "worker3", "group1", 2, home_site="group1") worker4 = Worker(Rota, "worker4", "group3", 3, home_site="group2") worker5 = Worker(Rota, "worker5", "group3", 3, home_site="group2") worker6 = Worker(Rota, "worker6", "group3", 3, home_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, constraints=[("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, constraints=[("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, constraints=[("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, constraints=[("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, constraints=[("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, constraints=[("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, constraints=[("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, constraints=[("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_home_site_presence(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=2, force_as_block=True, constraints=[("require_home_site_presence", ("group1", 2))], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("test9") 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("weekday_night") == limit assert self.Rota.results.solver.status == "ok" assert self.Rota.results.solver.termination_condition == "optimal" def test_constraint_require_home_site_presence2(self): self.Rota.shifts = [] self.Rota.add_shifts( SingleShift( ("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40, workers_required=3, force_as_block=True, constraints=[("require_home_site_presence", ("group2", 2))], ), ) self.Rota.build_and_solve(options={"ratio": 0.00}) self.Rota.export_rota_to_html("test9") 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)) if group == "group2": 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" # 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( Rota, "worker1", "group1", 1, not_available_to_work=(("15/03/22", "****"),) ) worker2 = Worker( Rota, "worker2", "group1", 1, not_available_to_work=(("14/03/22", "****"),) ) 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:], constraints=["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, constraints=["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, constraints=[] ), ) 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( self.Rota, "worker3", "group1", 1, not_available_to_work=(("13/03/22", "****"),) ) worker4 = Worker( self.Rota, "worker4", "group1", 1, not_available_to_work=(("12/03/22", "****"),) ) self.Rota.add_workers((worker3, worker4)) self.Rota.add_shifts( SingleShift( ("group1",), "night_weekend", 12.5, days[5:], workers_required=3, constraints=[] ), ) 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, "worker3", "group1", 1, not_available_to_work=(("13/03/22", "****"),) ) worker4 = Worker( self.Rota, "worker4", "group1", 1, not_available_to_work=(("12/03/22", "****"),) ) self.Rota.add_workers((worker3, worker4)) self.Rota.add_shifts( SingleShift( ("group1",), "night_weekend", 12.5, days[5:], workers_required=3, constraints=["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_home_site_presence()