.
This commit is contained in:
+1
-1
@@ -323,7 +323,7 @@ else:
|
|||||||
oop,
|
oop,
|
||||||
not_available_to_work=leave_requests,
|
not_available_to_work=leave_requests,
|
||||||
work_requests=work_requests,
|
work_requests=work_requests,
|
||||||
night_site=site_pref,
|
home_site=site_pref,
|
||||||
previous_shifts=previous_shifts,
|
previous_shifts=previous_shifts,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
shift_balance_extra=w["shift_balance_extra"],
|
shift_balance_extra=w["shift_balance_extra"],
|
||||||
|
|||||||
@@ -43,7 +43,23 @@ SHIFT_BOUNDS = {
|
|||||||
|
|
||||||
|
|
||||||
class SingleShift(object):
|
class SingleShift(object):
|
||||||
"""Class to hold all details for a shift"""
|
"""Class to hold all details for a shift
|
||||||
|
|
||||||
|
|
||||||
|
Valid constraints
|
||||||
|
|
||||||
|
balance_across_groups (requires block assignment)
|
||||||
|
|
||||||
|
postclear(2) / preclear(2)
|
||||||
|
|
||||||
|
limit_grade_number:
|
||||||
|
options: {grade: max_number, grade2: max_number2}
|
||||||
|
|
||||||
|
minimum_grade_number:
|
||||||
|
options: (grade, min_number)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -171,7 +187,6 @@ class RotaBuilder(object):
|
|||||||
self.max_weekend_frequency = max_weekend_frequency
|
self.max_weekend_frequency = max_weekend_frequency
|
||||||
|
|
||||||
self.constraint_options = {
|
self.constraint_options = {
|
||||||
"limit_to_1_st2_on_nights": True,
|
|
||||||
"ensure_1_st4_plus_on_nights": True,
|
"ensure_1_st4_plus_on_nights": True,
|
||||||
"ensure_derriford_reg_for_nights": True,
|
"ensure_derriford_reg_for_nights": True,
|
||||||
"balance_nights": True,
|
"balance_nights": True,
|
||||||
@@ -722,27 +737,6 @@ class RotaBuilder(object):
|
|||||||
# self.model.max_hours_constraint = Constraint(
|
# self.model.max_hours_constraint = Constraint(
|
||||||
# [worker.id for worker in self.workers], rule=maxHoursRule)
|
# [worker.id for worker in self.workers], rule=maxHoursRule)
|
||||||
|
|
||||||
# Limit to 1 ST2 (or below) on a night shift
|
|
||||||
def nightShiftMaxSTRule(model, week, shift):
|
|
||||||
"""Limits to 1 ST2 (or below) per night"""
|
|
||||||
single_workers = [w for w in self.workers if w.grade < 3]
|
|
||||||
if not single_workers:
|
|
||||||
return Constraint.Skip
|
|
||||||
return (
|
|
||||||
sum(
|
|
||||||
model.shift_week_worker_assigned[shift, week, w.id]
|
|
||||||
for w in single_workers
|
|
||||||
)
|
|
||||||
<= 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.constraint_options["limit_to_1_st2_on_nights"]:
|
|
||||||
self.model.night_shifts_max_st_constraint = Constraint(
|
|
||||||
[week for week in self.weeks],
|
|
||||||
[shift.name for shift in self.get_shifts_with_constraint("night")],
|
|
||||||
rule=nightShiftMaxSTRule,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Limit grades
|
# Limit grades
|
||||||
def limitGradesRule(model, week, shift, grade, limit):
|
def limitGradesRule(model, week, shift, grade, limit):
|
||||||
workers = [w for w in self.workers if w.grade == grade]
|
workers = [w for w in self.workers if w.grade == grade]
|
||||||
@@ -771,25 +765,34 @@ class RotaBuilder(object):
|
|||||||
#name=f"limit_grades_constraint_{shift.name}_{grade}"
|
#name=f"limit_grades_constraint_{shift.name}_{grade}"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def minimumGradesRule(model, week, shift, grade, limit):
|
||||||
def nightShiftMinST4Rule(model, week, shift):
|
workers = [w for w in self.workers if w.grade >= grade]
|
||||||
single_workers = [w for w in self.workers if w.grade >= 4]
|
if len(workers) < limit:
|
||||||
if not single_workers:
|
#return Constraint.Skip
|
||||||
return Constraint.Skip
|
raise ValueError("minimum_grade_number: not enough valid workers")
|
||||||
return (
|
return (
|
||||||
sum(
|
sum(
|
||||||
model.shift_week_worker_assigned[shift, week, w.id]
|
model.shift_week_worker_assigned[shift.name, week, w.id]
|
||||||
for w in single_workers
|
for w in workers
|
||||||
)
|
)
|
||||||
>= 1
|
>= limit
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.constraint_options["ensure_1_st4_plus_on_nights"]:
|
for shift in self.get_shifts_with_constraint("minimum_grade_number"):
|
||||||
self.model.night_shifts_min_st4_constraint = Constraint(
|
if not shift.constraint_options["minimum_grade_number"]:
|
||||||
|
raise ValueError("Constraint option must be defined for 'minimum_grade_number'")
|
||||||
|
grade, min_required = shift.constraint_options["minimum_grade_number"]
|
||||||
|
#self.model.limit_grades_constraint = Constraint(
|
||||||
|
setattr(self.model, f"minimum_grade_number_{shift.name}", Constraint(
|
||||||
[week for week in self.weeks],
|
[week for week in self.weeks],
|
||||||
[shift.name for shift in self.get_shifts_with_constraint("night")],
|
#[shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
|
||||||
rule=nightShiftMinST4Rule,
|
[shift],
|
||||||
)
|
[grade],
|
||||||
|
[min_required],
|
||||||
|
rule=minimumGradesRule,
|
||||||
|
#name=f"limit_grades_constraint_{shift.name}_{grade}"
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
# def nightShiftDerrifordRule(model, week, shift):
|
# def nightShiftDerrifordRule(model, week, shift):
|
||||||
# derriford_workers = [w for w in self.workers if w.night_at_derriford >= 1]
|
# derriford_workers = [w for w in self.workers if w.night_at_derriford >= 1]
|
||||||
@@ -810,9 +813,9 @@ class RotaBuilder(object):
|
|||||||
# rule=nightShiftDerrifordRule,
|
# rule=nightShiftDerrifordRule,
|
||||||
# )
|
# )
|
||||||
|
|
||||||
def presenceAtSiteOvernight(model, week, shift, required_site):
|
def presenceAtHomeSite(model, week, shift, required_site, required_number):
|
||||||
required_site_workers = [
|
required_site_workers = [
|
||||||
w for w in self.workers if required_site == w.night_site
|
w for w in self.workers if required_site == w.home_site
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -820,18 +823,20 @@ class RotaBuilder(object):
|
|||||||
model.shift_week_worker_assigned[shift, week, w.id]
|
model.shift_week_worker_assigned[shift, week, w.id]
|
||||||
for w in required_site_workers
|
for w in required_site_workers
|
||||||
)
|
)
|
||||||
>= 1
|
>= required_number
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.constraint_options["require_presence_at_site_overnight"]:
|
|
||||||
|
|
||||||
for site in self.constraint_options["require_presence_at_site_overnight"]:
|
for shift in self.get_shifts_with_constraint("require_home_site_presence"):
|
||||||
self.model.require_presence_at_site_overnight_rule = Constraint(
|
site, required_number = shift.constraint_options["require_home_site_presence"]
|
||||||
[week for week in self.weeks],
|
#self.model.require_presence_at_site_overnight_rule = Constraint(
|
||||||
[shift.name for shift in self.get_shifts_with_constraint("night")],
|
setattr(self.model, f"require_home_site_presence_{shift.name}", Constraint(
|
||||||
[site],
|
[week for week in self.weeks],
|
||||||
rule=presenceAtSiteOvernight,
|
[shift.name],
|
||||||
)
|
[site],
|
||||||
|
[required_number],
|
||||||
|
rule=presenceAtHomeSite,
|
||||||
|
))
|
||||||
|
|
||||||
# # Count the number of workers from each site on each night shift
|
# # Count the number of workers from each site on each night shift
|
||||||
# # As 1 or 0 is optimum we can simply subtract 1 from the number
|
# # As 1 or 0 is optimum we can simply subtract 1 from the number
|
||||||
|
|||||||
+160
-22
@@ -23,8 +23,6 @@ class TestDemoRota:
|
|||||||
Rota.constraint_options["max_shifts_per_week"] = 5
|
Rota.constraint_options["max_shifts_per_week"] = 5
|
||||||
Rota.constraint_options["max_shifts_per_month"] = 31
|
Rota.constraint_options["max_shifts_per_month"] = 31
|
||||||
Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
|
Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
|
||||||
Rota.constraint_options["limit_to_1_st2_on_nights"] = False
|
|
||||||
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = False
|
|
||||||
Rota.constraint_options["constrain_time_off_after_nights"] = False
|
Rota.constraint_options["constrain_time_off_after_nights"] = False
|
||||||
Rota.constraint_options["balance_nights_across_site"] = False
|
Rota.constraint_options["balance_nights_across_site"] = False
|
||||||
Rota.constraint_options["balance_shifts"] = True
|
Rota.constraint_options["balance_shifts"] = True
|
||||||
@@ -190,7 +188,6 @@ class TestDemoRotaNights:
|
|||||||
Rota.constraint_options["max_shifts_per_month"] = 31
|
Rota.constraint_options["max_shifts_per_month"] = 31
|
||||||
Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
|
Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
|
||||||
Rota.constraint_options["limit_to_1_st2_on_nights"] = False
|
Rota.constraint_options["limit_to_1_st2_on_nights"] = False
|
||||||
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = False
|
|
||||||
Rota.constraint_options["constrain_time_off_after_nights"] = False
|
Rota.constraint_options["constrain_time_off_after_nights"] = False
|
||||||
Rota.constraint_options["balance_nights_across_site"] = False
|
Rota.constraint_options["balance_nights_across_site"] = False
|
||||||
Rota.constraint_options["balance_shifts"] = True
|
Rota.constraint_options["balance_shifts"] = True
|
||||||
@@ -292,18 +289,8 @@ class TestDemoRotaClear:
|
|||||||
balance_offset_modifier=1,
|
balance_offset_modifier=1,
|
||||||
max_weekend_frequency=1,
|
max_weekend_frequency=1,
|
||||||
)
|
)
|
||||||
#Rota.constraint_options["max_shifts_per_week"] = 5
|
|
||||||
Rota.constraint_options["max_shifts_per_month"] = 20
|
Rota.constraint_options["max_shifts_per_month"] = 20
|
||||||
#Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
|
|
||||||
#Rota.constraint_options["limit_to_1_st2_on_nights"] = False
|
|
||||||
#Rota.constraint_options["ensure_1_st4_plus_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
|
|
||||||
|
|
||||||
worker1 = Worker(Rota, "worker1", "group1", 1)
|
worker1 = Worker(Rota, "worker1", "group1", 1)
|
||||||
worker2 = Worker(Rota, "worker2", "group1", 1)
|
worker2 = Worker(Rota, "worker2", "group1", 1)
|
||||||
@@ -549,12 +536,12 @@ class TestLimitConstraints:
|
|||||||
)
|
)
|
||||||
#Rota.constraint_options["max_shifts_per_week"] = 5
|
#Rota.constraint_options["max_shifts_per_week"] = 5
|
||||||
#Rota.constraint_options["max_shifts_per_month"] = 20
|
#Rota.constraint_options["max_shifts_per_month"] = 20
|
||||||
worker1 = Worker(Rota, "worker1", "group1", 2)
|
worker1 = Worker(Rota, "worker1", "group1", 2, home_site="group1")
|
||||||
worker2 = Worker(Rota, "worker2", "group1", 2)
|
worker2 = Worker(Rota, "worker2", "group1", 2, home_site="group1")
|
||||||
worker3 = Worker(Rota, "worker3", "group1", 2)
|
worker3 = Worker(Rota, "worker3", "group1", 2, home_site="group1")
|
||||||
worker4 = Worker(Rota, "worker4", "group3", 3)
|
worker4 = Worker(Rota, "worker4", "group3", 3, home_site="group2")
|
||||||
worker5 = Worker(Rota, "worker5", "group3", 3)
|
worker5 = Worker(Rota, "worker5", "group3", 3, home_site="group2")
|
||||||
worker6 = Worker(Rota, "worker6", "group3", 3)
|
worker6 = Worker(Rota, "worker6", "group3", 3, home_site="group2")
|
||||||
|
|
||||||
Rota.add_workers((worker1, worker2, worker3, worker4))
|
Rota.add_workers((worker1, worker2, worker3, worker4))
|
||||||
Rota.add_workers((worker5, worker6))
|
Rota.add_workers((worker5, worker6))
|
||||||
@@ -673,9 +660,160 @@ class TestLimitConstraints:
|
|||||||
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
self.Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
self.Rota.build_and_solve(options={"ratio": 0.1})
|
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")
|
self.Rota.export_rota_to_html("test9")
|
||||||
|
|
||||||
assert self.Rota.results.solver.termination_condition == "infeasible"
|
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:
|
#class TestNoWorkerRota:
|
||||||
# weeks_to_rota = 10
|
# weeks_to_rota = 10
|
||||||
@@ -693,4 +831,4 @@ class TestLimitConstraints:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
t = TestLimitConstraints()
|
t = TestLimitConstraints()
|
||||||
t.test_constraint_limit_grades4()
|
t.test_constraint_require_home_site_presence()
|
||||||
|
|||||||
+4
-4
@@ -20,7 +20,7 @@ class Worker:
|
|||||||
not_available_to_work=None,
|
not_available_to_work=None,
|
||||||
pref_not_to_work=None,
|
pref_not_to_work=None,
|
||||||
work_requests=None,
|
work_requests=None,
|
||||||
night_site: str = "plymouth", # We set a default proc_site
|
home_site: str = "plymouth", # We set a default proc_site
|
||||||
previous_shifts: dict = {},
|
previous_shifts: dict = {},
|
||||||
shift_balance_extra: dict = {},
|
shift_balance_extra: dict = {},
|
||||||
bank_holiday_extra: int = 0,
|
bank_holiday_extra: int = 0,
|
||||||
@@ -44,8 +44,8 @@ class Worker:
|
|||||||
self.shift_balance_extra = shift_balance_extra
|
self.shift_balance_extra = shift_balance_extra
|
||||||
self.bank_holiday_extra = bank_holiday_extra
|
self.bank_holiday_extra = bank_holiday_extra
|
||||||
|
|
||||||
self.proc_site = night_site
|
self.home_site = home_site
|
||||||
if night_site == "plymouth" or night_site == "":
|
if home_site == "plymouth" or home_site == "":
|
||||||
self.night_at_derriford = 1
|
self.night_at_derriford = 1
|
||||||
else:
|
else:
|
||||||
self.night_at_derriford = 0
|
self.night_at_derriford = 0
|
||||||
@@ -172,7 +172,7 @@ class Worker:
|
|||||||
self.name,
|
self.name,
|
||||||
(self.site, self.grade, self.fte_adj),
|
(self.site, self.grade, self.fte_adj),
|
||||||
nwd,
|
nwd,
|
||||||
self.proc_site,
|
self.home_site,
|
||||||
self.night_at_derriford,
|
self.night_at_derriford,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user