fair shift distribution, no overlapping shifts
This commit is contained in:
parent
6e31bb1378
commit
3f07acfbd5
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue