Add weekend day balancing options and summary counts for shifts
This commit is contained in:
@@ -342,6 +342,8 @@ def main(
|
|||||||
balance_weekends=True,
|
balance_weekends=True,
|
||||||
max_shifts_per_week=6,
|
max_shifts_per_week=6,
|
||||||
max_days_per_week_block=[(4, 2), (4, 3)],
|
max_days_per_week_block=[(4, 2), (4, 3)],
|
||||||
|
balance_weekend_days=True,
|
||||||
|
#weekend_day_hard_max_deviation=2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -173,9 +173,21 @@ function generateExtra() {
|
|||||||
worker_td.get(0).dataset.shiftDiffSummed = summed_shift_diff.toPrecision(2);
|
worker_td.get(0).dataset.shiftDiffSummed = summed_shift_diff.toPrecision(2);
|
||||||
|
|
||||||
|
|
||||||
|
// compute separate Sat / Sun counts for this worker
|
||||||
|
let sat_count = 0;
|
||||||
|
let sun_count = 0;
|
||||||
|
for (let i = 1; i <= weeks; i++) {
|
||||||
|
let sat = $(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift");
|
||||||
|
let sun = $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift");
|
||||||
|
if (sat && sat !== "") sat_count += 1;
|
||||||
|
if (sun && sun !== "") sun_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
worker_td.after(`<div class='worker-summary auto-generated'>
|
worker_td.after(`<div class='worker-summary auto-generated'>
|
||||||
<span>Total shifts: ${total_shifts}, </span>
|
<span>Total shifts: ${total_shifts}, </span>
|
||||||
<span>Weekends: ${weekends_worked}, </span>
|
<span>Weekends: ${weekends_worked}, </span>
|
||||||
|
<span>Sat: ${sat_count}, </span>
|
||||||
|
<span>Sun: ${sun_count}, </span>
|
||||||
<span>Nights: ${nights_worked}, </span>
|
<span>Nights: ${nights_worked}, </span>
|
||||||
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
||||||
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
|
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
|
||||||
|
|||||||
+269
-2
@@ -345,6 +345,19 @@ class RotaConstraintOptions(BaseModel):
|
|||||||
maximum_allowed_shift_diff: Optional[int] = Field(None, description="Maximum allowed per-worker shift difference (None = no limit)")
|
maximum_allowed_shift_diff: Optional[int] = Field(None, description="Maximum allowed per-worker shift difference (None = no limit)")
|
||||||
balance_weekends: bool = Field(True, description="Balance weekend allocations")
|
balance_weekends: bool = Field(True, description="Balance weekend allocations")
|
||||||
max_weekends: int = Field(100, description="Maximum number of weekends considered")
|
max_weekends: int = Field(100, description="Maximum number of weekends considered")
|
||||||
|
# If true, try to balance number of weekend days worked per worker separately
|
||||||
|
# for Saturday and Sunday (i.e. equivalent workers should have similar Sat counts
|
||||||
|
# and similar Sun counts).
|
||||||
|
balance_weekend_days: bool = Field(False, description="Balance weekend days (Sat/Sun) separately")
|
||||||
|
balance_weekend_days_weight: int = Field(5, description="Objective weight for balancing weekend days per day")
|
||||||
|
balance_weekend_days_quadratic: bool = Field(False, description="Use quadratic penalty to balance weekend days per day")
|
||||||
|
weekend_day_hard_max_deviation: Optional[int] = Field(
|
||||||
|
None,
|
||||||
|
description=(
|
||||||
|
"Hard maximum allowed absolute deviation (integer) per-worker from the per-day "
|
||||||
|
"weekend target (Sat/Sun). If set, adds constraints enforcing |assigned - target| <= value"
|
||||||
|
),
|
||||||
|
)
|
||||||
max_weekend_frequency: int = Field(1, description="Don't assign multiple shifts every N weeks (requires balance_weekends)")
|
max_weekend_frequency: int = Field(1, description="Don't assign multiple shifts every N weeks (requires balance_weekends)")
|
||||||
max_night_frequency: int = Field(1, description="Don't assign multiple night shifts every N weeks")
|
max_night_frequency: int = Field(1, description="Don't assign multiple night shifts every N weeks")
|
||||||
max_night_frequency_week_exclusions: List[int] = Field(default_factory=list, description="Weeks to exclude from night frequency checks")
|
max_night_frequency_week_exclusions: List[int] = Field(default_factory=list, description="Weeks to exclude from night frequency checks")
|
||||||
@@ -421,7 +434,6 @@ class RotaBuilder(object):
|
|||||||
# None (use defaults), a dict (raw values), or an existing model instance.
|
# None (use defaults), a dict (raw values), or an existing model instance.
|
||||||
# Store the model instance in `self.constraint_options_model` and also
|
# Store the model instance in `self.constraint_options_model` and also
|
||||||
# expose it as `self.constraint_options` for minimal code changes.
|
# expose it as `self.constraint_options` for minimal code changes.
|
||||||
constraint_options = None
|
|
||||||
if constraint_options is None:
|
if constraint_options is None:
|
||||||
opts_model = RotaConstraintOptions()
|
opts_model = RotaConstraintOptions()
|
||||||
elif isinstance(constraint_options, RotaConstraintOptions):
|
elif isinstance(constraint_options, RotaConstraintOptions):
|
||||||
@@ -433,6 +445,8 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
self.constraint_options_model: RotaConstraintOptions = opts_model
|
self.constraint_options_model: RotaConstraintOptions = opts_model
|
||||||
|
|
||||||
|
logger.debug(f"Rota constraint options: {self.constraint_options_model.model_dump()}")
|
||||||
|
|
||||||
self.terminate_on_warning = [
|
self.terminate_on_warning = [
|
||||||
"Worker/duplicate id",
|
"Worker/duplicate id",
|
||||||
"Worker/duplicate name",
|
"Worker/duplicate name",
|
||||||
@@ -1014,6 +1028,37 @@ class RotaBuilder(object):
|
|||||||
initialize=0,
|
initialize=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Per-day weekend balancing (separate Sat / Sun counts)
|
||||||
|
# Also create the bookkeeping variables when a hard deviation constraint is requested
|
||||||
|
if (
|
||||||
|
self.constraint_options_model.balance_weekend_days
|
||||||
|
or self.constraint_options_model.balance_weekend_days_quadratic
|
||||||
|
or self.constraint_options_model.weekend_day_hard_max_deviation is not None
|
||||||
|
):
|
||||||
|
self.model.weekend_day_shift_count = Var(
|
||||||
|
((worker.id, d) for worker in self.workers for d in ("Sat", "Sun")),
|
||||||
|
domain=NonNegativeReals,
|
||||||
|
initialize=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.model.weekend_day_shift_count_t1 = Var(
|
||||||
|
((worker.id, d) for worker in self.workers for d in ("Sat", "Sun")),
|
||||||
|
domain=NonNegativeReals,
|
||||||
|
initialize=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.model.weekend_day_shift_count_t2 = Var(
|
||||||
|
((worker.id, d) for worker in self.workers for d in ("Sat", "Sun")),
|
||||||
|
domain=NonNegativeReals,
|
||||||
|
initialize=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.model.weekend_day_shift_count_w = Var(
|
||||||
|
((worker.id, d) for worker in self.workers for d in ("Sat", "Sun")),
|
||||||
|
domain=NonNegativeReals,
|
||||||
|
initialize=0,
|
||||||
|
)
|
||||||
|
|
||||||
# self.model.weekend_count_t1 = Var(
|
# self.model.weekend_count_t1 = Var(
|
||||||
# ((worker.id) for worker in self.workers),
|
# ((worker.id) for worker in self.workers),
|
||||||
# domain=NonNegativeReals,
|
# domain=NonNegativeReals,
|
||||||
@@ -2459,6 +2504,99 @@ class RotaBuilder(object):
|
|||||||
- xU * xU
|
- xU * xU
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Per-day weekend balancing (Sat and Sun separately)
|
||||||
|
logger.debug(f"Adding per-day weekend balancing constraints for worker {worker.name} ({worker.id})")
|
||||||
|
logger.debug(self.constraint_options_model.weekend_day_hard_max_deviation)
|
||||||
|
if (
|
||||||
|
self.constraint_options_model.balance_weekend_days
|
||||||
|
or self.constraint_options_model.balance_weekend_days_quadratic
|
||||||
|
or self.constraint_options_model.weekend_day_hard_max_deviation is not None
|
||||||
|
):
|
||||||
|
logger.debug(f"Worker {worker.name}: per-day weekend block active (hard_dev={self.constraint_options_model.weekend_day_hard_max_deviation})")
|
||||||
|
console.print(f"Worker {worker.name}: per-day weekend block active (hard_dev={self.constraint_options_model.weekend_day_hard_max_deviation})")
|
||||||
|
hard_weekend_constraints_added = 0
|
||||||
|
for day_name in ("Sat", "Sun"):
|
||||||
|
# count assignments on this specific weekend day
|
||||||
|
self.model.constraints.add(
|
||||||
|
self.model.weekend_day_shift_count[worker.id, day_name]
|
||||||
|
== sum(
|
||||||
|
self.model.works[worker.id, w, d, shift]
|
||||||
|
for w, d, shift in self.get_all_shiftname_combinations()
|
||||||
|
if d == day_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# target number for this day (derived from shift targets)
|
||||||
|
weekend_day_shift_target_number = sum(
|
||||||
|
worker.shift_target_number[shift.name] / len(shift.days)
|
||||||
|
for shift in self.get_shifts()
|
||||||
|
if day_name in shift.days
|
||||||
|
)
|
||||||
|
|
||||||
|
self.model.constraints.add(
|
||||||
|
self.model.weekend_day_shift_count_t1[worker.id, day_name]
|
||||||
|
- self.model.weekend_day_shift_count_t2[worker.id, day_name]
|
||||||
|
== self.model.weekend_day_shift_count[worker.id, day_name]
|
||||||
|
- weekend_day_shift_target_number
|
||||||
|
)
|
||||||
|
|
||||||
|
xU_day = self.constraint_options_model.max_weekends
|
||||||
|
xL_day = 1
|
||||||
|
self.model.constraints.add(
|
||||||
|
inequality(
|
||||||
|
xL_day,
|
||||||
|
self.model.weekend_day_shift_count_t1[worker.id, day_name]
|
||||||
|
+ self.model.weekend_day_shift_count_t2[worker.id, day_name]
|
||||||
|
+ 1,
|
||||||
|
xU_day,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.model.constraints.add(
|
||||||
|
self.model.weekend_day_shift_count_w[worker.id, day_name]
|
||||||
|
>= xL_day
|
||||||
|
* (
|
||||||
|
self.model.weekend_day_shift_count_t1[worker.id, day_name]
|
||||||
|
+ self.model.weekend_day_shift_count_t2[worker.id, day_name]
|
||||||
|
+ 1
|
||||||
|
)
|
||||||
|
* 2
|
||||||
|
- xL_day * xL_day
|
||||||
|
)
|
||||||
|
|
||||||
|
self.model.constraints.add(
|
||||||
|
self.model.weekend_day_shift_count_w[worker.id, day_name]
|
||||||
|
>= xU_day
|
||||||
|
* (
|
||||||
|
self.model.weekend_day_shift_count_t1[worker.id, day_name]
|
||||||
|
+ self.model.weekend_day_shift_count_t2[worker.id, day_name]
|
||||||
|
+ 1
|
||||||
|
)
|
||||||
|
* 2
|
||||||
|
- xU_day * xU_day
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional hard constraint: limit absolute deviation from per-day target
|
||||||
|
if self.constraint_options_model.weekend_day_hard_max_deviation is not None:
|
||||||
|
d = self.constraint_options_model.weekend_day_hard_max_deviation
|
||||||
|
console.print(f"Adding hard weekend-day deviation constraint: worker={worker.name} ({worker.id}), day={day_name}, target={weekend_day_shift_target_number:.3f}, d={d}")
|
||||||
|
# assigned - target <= d
|
||||||
|
self.model.constraints.add(
|
||||||
|
self.model.weekend_day_shift_count[worker.id, day_name]
|
||||||
|
- weekend_day_shift_target_number
|
||||||
|
<= d
|
||||||
|
)
|
||||||
|
# target - assigned <= d
|
||||||
|
self.model.constraints.add(
|
||||||
|
weekend_day_shift_target_number
|
||||||
|
- self.model.weekend_day_shift_count[worker.id, day_name]
|
||||||
|
<= d
|
||||||
|
)
|
||||||
|
hard_weekend_constraints_added += 2
|
||||||
|
|
||||||
|
if hard_weekend_constraints_added:
|
||||||
|
console.print(f"Worker {worker.name}: added {hard_weekend_constraints_added} hard weekend-day constraints")
|
||||||
|
|
||||||
# Ensure worker is not allocated shifts on non working days
|
# Ensure worker is not allocated shifts on non working days
|
||||||
if worker.non_working_day_list:
|
if worker.non_working_day_list:
|
||||||
for week, day, shift in self.get_all_shiftclass_combinations():
|
for week, day, shift in self.get_all_shiftclass_combinations():
|
||||||
@@ -3380,6 +3518,33 @@ class RotaBuilder(object):
|
|||||||
else:
|
else:
|
||||||
weekend_shift_balancing = 0
|
weekend_shift_balancing = 0
|
||||||
|
|
||||||
|
# Balance each weekend day (Sat / Sun) separately if requested.
|
||||||
|
# Support either a linear term or a stronger quadratic penalty.
|
||||||
|
weekend_day_term = 0
|
||||||
|
if self.constraint_options_model.balance_weekend_days_quadratic:
|
||||||
|
wd_weight = self.constraint_options_model.balance_weekend_days_weight
|
||||||
|
# Quadratic penalty: strongly discourage deviations from per-day targets
|
||||||
|
weekend_day_term = sum(
|
||||||
|
wd_weight
|
||||||
|
* (
|
||||||
|
self.model.weekend_day_shift_count[(worker.id, day_name)]
|
||||||
|
- sum(
|
||||||
|
worker.shift_target_number[shift.name] / len(shift.days)
|
||||||
|
for shift in self.get_shifts()
|
||||||
|
if day_name in shift.days
|
||||||
|
)
|
||||||
|
) ** 2
|
||||||
|
for worker in self.workers
|
||||||
|
for day_name in ("Sat", "Sun")
|
||||||
|
)
|
||||||
|
elif self.constraint_options_model.balance_weekend_days:
|
||||||
|
weekend_day_modifier = self.constraint_options_model.balance_weekend_days_weight
|
||||||
|
weekend_day_term = sum(
|
||||||
|
weekend_day_modifier * self.model.weekend_day_shift_count_w[(worker.id, day_name)]
|
||||||
|
for worker in self.workers
|
||||||
|
for day_name in ("Sat", "Sun")
|
||||||
|
)
|
||||||
|
|
||||||
preference_constant = 10
|
preference_constant = 10
|
||||||
# Preferences (not to work)
|
# Preferences (not to work)
|
||||||
preferences = sum(
|
preferences = sum(
|
||||||
@@ -3479,6 +3644,7 @@ class RotaBuilder(object):
|
|||||||
+ true_quadratic_shift_balancing
|
+ true_quadratic_shift_balancing
|
||||||
- prefer_multi_shift_expr
|
- prefer_multi_shift_expr
|
||||||
+ prefer_block_expr
|
+ prefer_block_expr
|
||||||
|
+ weekend_day_term
|
||||||
)
|
)
|
||||||
|
|
||||||
# add objective function to the model. rule (pass function) or expr (pass expression directly)
|
# add objective function to the model. rule (pass function) or expr (pass expression directly)
|
||||||
@@ -3566,6 +3732,47 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
self.workers = sorted(self.workers)
|
self.workers = sorted(self.workers)
|
||||||
|
|
||||||
|
# Pairwise hard constraints to limit per-day weekend skew among equivalent workers
|
||||||
|
# Group workers by (site, grade) and enforce for each pair and each day:
|
||||||
|
# assigned_i_day - assigned_j_day <= d
|
||||||
|
# assigned_j_day - assigned_i_day <= d
|
||||||
|
# Use `weekend_day_hard_max_deviation` as `d` when set.
|
||||||
|
if self.constraint_options_model.weekend_day_hard_max_deviation is not None:
|
||||||
|
d = self.constraint_options_model.weekend_day_hard_max_deviation
|
||||||
|
groups = defaultdict(list)
|
||||||
|
for w in self.workers:
|
||||||
|
groups[(w.site, w.grade)].append(w)
|
||||||
|
|
||||||
|
pairwise_added = 0
|
||||||
|
for (site, grade), workers_group in groups.items():
|
||||||
|
if len(workers_group) < 2:
|
||||||
|
continue
|
||||||
|
# iterate unique unordered pairs
|
||||||
|
for i in range(len(workers_group)):
|
||||||
|
for j in range(i + 1, len(workers_group)):
|
||||||
|
wi = workers_group[i]
|
||||||
|
wj = workers_group[j]
|
||||||
|
for day_name in ("Sat", "Sun"):
|
||||||
|
try:
|
||||||
|
# wi - wj <= d
|
||||||
|
self.model.constraints.add(
|
||||||
|
self.model.weekend_day_shift_count[wi.id, day_name]
|
||||||
|
- self.model.weekend_day_shift_count[wj.id, day_name]
|
||||||
|
<= d
|
||||||
|
)
|
||||||
|
# wj - wi <= d
|
||||||
|
self.model.constraints.add(
|
||||||
|
self.model.weekend_day_shift_count[wj.id, day_name]
|
||||||
|
- self.model.weekend_day_shift_count[wi.id, day_name]
|
||||||
|
<= d
|
||||||
|
)
|
||||||
|
pairwise_added += 2
|
||||||
|
except Exception:
|
||||||
|
# If variables aren't present for some worker/day, skip
|
||||||
|
continue
|
||||||
|
|
||||||
|
console.print(f"Added {pairwise_added} pairwise weekend-day hard constraints (d={d})")
|
||||||
|
|
||||||
self.full_time_equivalent = sum(
|
self.full_time_equivalent = sum(
|
||||||
w.fte_adj for w in self.workers
|
w.fte_adj for w in self.workers
|
||||||
) # TODO: rewrite as per shift
|
) # TODO: rewrite as per shift
|
||||||
@@ -4438,6 +4645,21 @@ class RotaBuilder(object):
|
|||||||
shift_count
|
shift_count
|
||||||
+ f"#weekends_worked: {model.worker_weekend_count[worker.id].value}\\#"
|
+ f"#weekends_worked: {model.worker_weekend_count[worker.id].value}\\#"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# compute assigned Saturday / Sunday counts for this worker
|
||||||
|
sat_count = 0
|
||||||
|
sun_count = 0
|
||||||
|
for w, d in self.get_week_day_combinations():
|
||||||
|
if d == "Sat":
|
||||||
|
for s in self.get_shift_names_by_week_day(w, d, return_empty_if_week_day_not_found=True):
|
||||||
|
if model.works[worker.id, w, d, s].value > 0.5:
|
||||||
|
sat_count += 1
|
||||||
|
break
|
||||||
|
if d == "Sun":
|
||||||
|
for s in self.get_shift_names_by_week_day(w, d, return_empty_if_week_day_not_found=True):
|
||||||
|
if model.works[worker.id, w, d, s].value > 0.5:
|
||||||
|
sun_count += 1
|
||||||
|
break
|
||||||
timetable.append(f"{''.join(w)} {shift_count}")
|
timetable.append(f"{''.join(w)} {shift_count}")
|
||||||
|
|
||||||
if show_prefs:
|
if show_prefs:
|
||||||
@@ -4661,6 +4883,28 @@ class RotaBuilder(object):
|
|||||||
diff = model.shift_count_diff[worker.id, shift.name].value
|
diff = model.shift_count_diff[worker.id, shift.name].value
|
||||||
shift_diff_dict[shift.name] = diff
|
shift_diff_dict[shift.name] = diff
|
||||||
|
|
||||||
|
# compute assigned Saturday / Sunday counts for this worker (for HTML data attributes)
|
||||||
|
sat_count = 0
|
||||||
|
sun_count = 0
|
||||||
|
for w, d in self.get_week_day_combinations():
|
||||||
|
if d == "Sat":
|
||||||
|
for s in self.get_shift_names_by_week_day(w, d, return_empty_if_week_day_not_found=True):
|
||||||
|
if model.works[worker.id, w, d, s].value > 0.5:
|
||||||
|
sat_count += 1
|
||||||
|
break
|
||||||
|
if d == "Sun":
|
||||||
|
for s in self.get_shift_names_by_week_day(w, d, return_empty_if_week_day_not_found=True):
|
||||||
|
if model.works[worker.id, w, d, s].value > 0.5:
|
||||||
|
sun_count += 1
|
||||||
|
break
|
||||||
|
# Small HTML summary to be inserted inside the worker td (friendly display)
|
||||||
|
worker_summary_html = (
|
||||||
|
f"<div class='worker-summary auto-generated' style='margin-top:4px; font-size:0.9em; color:#333;'>"
|
||||||
|
f"<span class=\'weekend-sat\'>Sat: <strong>{sat_count}</strong></span>"
|
||||||
|
f" <span class=\'weekend-sun\'>Sun: <strong>{sun_count}</strong></span>"
|
||||||
|
f"</div>"
|
||||||
|
)
|
||||||
|
|
||||||
worker_td = """<td title='Site: {site}' class='worker {site}'
|
worker_td = """<td title='Site: {site}' class='worker {site}'
|
||||||
data-worker-id='{worker_id}'
|
data-worker-id='{worker_id}'
|
||||||
data-nwds='{nwds}' data-site='{site}' data-worker='{name}'
|
data-nwds='{nwds}' data-site='{site}' data-worker='{name}'
|
||||||
@@ -4676,10 +4920,12 @@ class RotaBuilder(object):
|
|||||||
data-pair='{pair}'
|
data-pair='{pair}'
|
||||||
data-shift-balance-extra='{shift_balance_extra}'
|
data-shift-balance-extra='{shift_balance_extra}'
|
||||||
data-bank-holiday-extra='{bank_holiday_extra}'
|
data-bank-holiday-extra='{bank_holiday_extra}'
|
||||||
|
data-weekend-sat='{weekend_sat}'
|
||||||
|
data-weekend-sun='{weekend_sun}'
|
||||||
data-shift-diff='{shift_diff}'
|
data-shift-diff='{shift_diff}'
|
||||||
data-shift-diff-summed='{shift_diff_summed}'
|
data-shift-diff-summed='{shift_diff_summed}'
|
||||||
>
|
>
|
||||||
<span class='name' title='{name} ({worker_id})'>{name}</span> ({grade}) [{fte}]</td>""".format(
|
<span class='name' title='{name} ({worker_id})'>{name}</span> ({grade}) [{fte}]{worker_summary_html}</td>""".format(
|
||||||
site=worker.site,
|
site=worker.site,
|
||||||
nwds=nwds,
|
nwds=nwds,
|
||||||
worker_id=worker.id,
|
worker_id=worker.id,
|
||||||
@@ -4695,6 +4941,9 @@ class RotaBuilder(object):
|
|||||||
weekend_target=worker.weekend_shift_target_number,
|
weekend_target=worker.weekend_shift_target_number,
|
||||||
remote_site=worker.remote_site,
|
remote_site=worker.remote_site,
|
||||||
grade=worker.grade,
|
grade=worker.grade,
|
||||||
|
weekend_sat=sat_count,
|
||||||
|
weekend_sun=sun_count,
|
||||||
|
worker_summary_html=worker_summary_html,
|
||||||
pair=worker.pair,
|
pair=worker.pair,
|
||||||
shift_balance_extra=json.dumps(worker.shift_balance_extra),
|
shift_balance_extra=json.dumps(worker.shift_balance_extra),
|
||||||
shift_diff=json.dumps(shift_diff_dict),
|
shift_diff=json.dumps(shift_diff_dict),
|
||||||
@@ -4788,6 +5037,24 @@ class RotaBuilder(object):
|
|||||||
f"Unavailable: {''}",
|
f"Unavailable: {''}",
|
||||||
f"Shift targets: {json.dumps(getattr(worker, 'shift_target_number', {}))}",
|
f"Shift targets: {json.dumps(getattr(worker, 'shift_target_number', {}))}",
|
||||||
]
|
]
|
||||||
|
# compute Sat / Sun assigned counts for this worker
|
||||||
|
sat_count = 0
|
||||||
|
sun_count = 0
|
||||||
|
for w, d in self.get_week_day_combinations():
|
||||||
|
if d == "Sat":
|
||||||
|
for s in self.get_shift_names_by_week_day(w, d, return_empty_if_week_day_not_found=True):
|
||||||
|
if self.model.works[worker.id, w, d, s].value > 0.5:
|
||||||
|
sat_count += 1
|
||||||
|
break
|
||||||
|
if d == "Sun":
|
||||||
|
for s in self.get_shift_names_by_week_day(w, d, return_empty_if_week_day_not_found=True):
|
||||||
|
if self.model.works[worker.id, w, d, s].value > 0.5:
|
||||||
|
sun_count += 1
|
||||||
|
break
|
||||||
|
details.insert(
|
||||||
|
-1,
|
||||||
|
f"Sat assigned: {sat_count}, Sun assigned: {sun_count}",
|
||||||
|
)
|
||||||
# Build unavailability list for this worker
|
# Build unavailability list for this worker
|
||||||
unavail_list = []
|
unavail_list = []
|
||||||
for entry in sorted(self.unavailable_to_work):
|
for entry in sorted(self.unavailable_to_work):
|
||||||
|
|||||||
Reference in New Issue
Block a user