2
0
Fork 0

fair shift distribution, no overlapping shifts

This commit is contained in:
Andreas (@xAndy) Zimmermann 2023-05-13 16:25:33 +02:00
parent 6e31bb1378
commit 3f07acfbd5
1 changed files with 43 additions and 6 deletions

View File

@ -1,6 +1,8 @@
from shiftregister.importer.models import *
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
night_shift_query = Q(start_at__hour__gte=21) | Q(start_at__hour__lte=10)
@ -49,10 +51,35 @@ class TeamMember(models.Model):
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}"
)
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:
shifts = free_bucket.order_by("?")[:shift_count]
for shift in shifts:
assigned_shift_count = 0
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)
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
# 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):
# 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:
# 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...
@ -93,14 +120,20 @@ class TeamMember(models.Model):
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...
assignment = (
FallbackAssignment.objects.filter(
team_member_id=member.id, shift_id__in=canidate_shift_ids
FallbackAssignment.objects.annotate(
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(
shift_id__in=FallbackAssignment.objects.filter(
team_member_id=self.pk
).values("shift_id")
)
.filter(*blocked_times)
.order_by("?")
.first()
)
@ -110,6 +143,10 @@ class TeamMember(models.Model):
print("could not find any matching assignments to take away")
return
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)
assignment.delete()