fair shift distribution, no overlapping shifts

pull/1/head
parent 6e31bb1378
commit 3f07acfbd5

@ -1,6 +1,8 @@
from shiftregister.importer.models import * from shiftregister.importer.models import *
from django.db.models import Max, Sum from django.db.models import Max, Sum
from django.db.models import Count, Exists, OuterRef, Subquery, Func from django.db.models import Count, Exists, OuterRef, ExpressionWrapper
from django.db.models.lookups import LessThan
from django.db.models.fields import DateTimeField
import math import math
night_shift_query = Q(start_at__hour__gte=21) | Q(start_at__hour__lte=10) night_shift_query = Q(start_at__hour__gte=21) | Q(start_at__hour__lte=10)
@ -49,10 +51,35 @@ class TeamMember(models.Model):
print( print(
f"total:{total_slot_count} max:{max_shifts_per_member} calc:{shifts_per_member} chosen:{shift_count} calc:{active_team_members*shift_count} free `before:{free_slot_count} {self.name}" f"total:{total_slot_count} max:{max_shifts_per_member} calc:{shifts_per_member} chosen:{shift_count} calc:{active_team_members*shift_count} free `before:{free_slot_count} {self.name}"
) )
blocked_times = []
for shift in self.fallback_shifts.all():
blocked_times.append(
Q(start_at__gte=(shift.start_at + shift.duration))
| Q(end_at__lte=shift.start_at)
)
# easy part: enough free shifts for everyone: # easy part: enough free shifts for everyone:
shifts = free_bucket.order_by("?")[:shift_count] assigned_shift_count = 0
for shift in shifts: for _ in range(shift_count):
shift = (
free_bucket.annotate(
end_at=ExpressionWrapper(
F("start_at") + F("duration"),
output_field=models.DateTimeField(),
)
)
.filter(*blocked_times)
.order_by("?")
.first()
)
if not shift:
break
self.fallback_shifts.add(shift) self.fallback_shifts.add(shift)
assigned_shift_count += 1
blocked_times.append(
Q(start_at__gte=(shift.start_at + shift.duration))
| Q(end_at__lte=shift.start_at)
)
# blocked_times.append(Q(start_at__gte=(shift.start_at+shift.duration)) | LessThan(F('start_at')+F('duration'), shift.start_at, output_field=DateTimeField()))
# there is a chance that even if qota*teammembers team members are activatet, there are still unasigned shifts # there is a chance that even if qota*teammembers team members are activatet, there are still unasigned shifts
# this happens if there are shifts with multiple people left, as we can not assign multiple slots for # this happens if there are shifts with multiple people left, as we can not assign multiple slots for
@ -61,7 +88,7 @@ class TeamMember(models.Model):
# if len(shifts) >= (shift_count - 1): # if len(shifts) >= (shift_count - 1):
# return # return
shifts_needed = shift_count - len(shifts) shifts_needed = shift_count - assigned_shift_count
# this is not done very often so we can do this kinda inefficient but readable and maintainable: # this is not done very often so we can do this kinda inefficient but readable and maintainable:
# for each missing shift, get the team member who has the most shifts in our bucket and take one of them at random # for each missing shift, get the team member who has the most shifts in our bucket and take one of them at random
# but also take care to not take any slots for shifts we already have... # but also take care to not take any slots for shifts we already have...
@ -93,14 +120,20 @@ class TeamMember(models.Model):
for member in sorted_members: for member in sorted_members:
# now get all their assignments in the relevant bucket but exclude the ones where we already have a slot in the same shift... # now get all their assignments in the relevant bucket but exclude the ones where we already have a slot in the same shift...
assignment = ( assignment = (
FallbackAssignment.objects.filter( FallbackAssignment.objects.annotate(
team_member_id=member.id, shift_id__in=canidate_shift_ids start_at=F("shift__start_at"),
end_at=ExpressionWrapper(
F("shift__start_at") + F("shift__duration"),
output_field=models.DateTimeField(),
),
) )
.filter(team_member_id=member.id, shift_id__in=canidate_shift_ids)
.exclude( .exclude(
shift_id__in=FallbackAssignment.objects.filter( shift_id__in=FallbackAssignment.objects.filter(
team_member_id=self.pk team_member_id=self.pk
).values("shift_id") ).values("shift_id")
) )
.filter(*blocked_times)
.order_by("?") .order_by("?")
.first() .first()
) )
@ -110,6 +143,10 @@ class TeamMember(models.Model):
print("could not find any matching assignments to take away") print("could not find any matching assignments to take away")
return return
shifts_needed -= 1 shifts_needed -= 1
blocked_times.append(
Q(start_at__gte=(assignment.shift.start_at + assignment.shift.duration))
| Q(end_at__lte=assignment.shift.start_at)
)
self.fallback_shifts.add(assignment.shift) self.fallback_shifts.add(assignment.shift)
assignment.delete() assignment.delete()

Loading…
Cancel
Save