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 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…
Reference in New Issue