195 lines
10 KiB
Python
195 lines
10 KiB
Python
from datetime import date
|
||
|
||
from django.contrib.auth import get_user_model
|
||
from rest_framework import serializers
|
||
|
||
from notes.serializers import NoteUserSerializer
|
||
from users.permissions import require_permission
|
||
from .models import WorkSchedule, WorkScheduleSlot, ScheduleException, ExceptionSlot, DayChangeRequest
|
||
|
||
|
||
def checked_dates(start, end):
|
||
if start < date(2000, 1, 1) or start > date(2099, 1, 1) or (end and (end < start or end > date(2099, 12, 31))):
|
||
raise serializers.ValidationError('Choisissez des dates valides, dans l’ordre, entre 2000 et 2099.')
|
||
|
||
|
||
class SlotInput(serializers.Serializer):
|
||
start_time = serializers.TimeField(input_formats=['%H:%M', '%H:%M:%S'])
|
||
end_time = serializers.TimeField(input_formats=['%H:%M', '%H:%M:%S'])
|
||
end_day_offset = serializers.IntegerField(min_value=0, max_value=1, default=0)
|
||
|
||
def validate(self, attrs):
|
||
start, end = attrs['start_time'], attrs['end_time']
|
||
if start.second or end.second or start.microsecond or end.microsecond:
|
||
raise serializers.ValidationError('Les horaires doivent être précis à la minute.')
|
||
duration = (end.hour * 60 + end.minute) + attrs['end_day_offset'] * 1440 - (start.hour * 60 + start.minute)
|
||
if not 0 < duration <= 1440:
|
||
raise serializers.ValidationError('Une plage de travail doit durer entre une minute et 24 heures. Vérifiez le jour et l’heure de fin.')
|
||
return attrs
|
||
|
||
|
||
class WeeklySlotInput(SlotInput):
|
||
weekday = serializers.IntegerField(min_value=0, max_value=6)
|
||
week_index = serializers.IntegerField(min_value=0, max_value=51, default=0)
|
||
|
||
|
||
|
||
|
||
class DayTargetInput(serializers.Serializer):
|
||
weekday = serializers.IntegerField(min_value=0, max_value=6)
|
||
week_index = serializers.IntegerField(min_value=0, max_value=51, default=0)
|
||
minutes = serializers.IntegerField(min_value=0, max_value=1440)
|
||
|
||
|
||
class WeekTargetInput(serializers.Serializer):
|
||
week_index = serializers.IntegerField(min_value=0, max_value=51)
|
||
minutes = serializers.IntegerField(min_value=0, max_value=10080)
|
||
|
||
|
||
class ScheduleInput(serializers.Serializer):
|
||
user_id = serializers.PrimaryKeyRelatedField(source='user', queryset=get_user_model().objects.filter(is_active=True))
|
||
valid_from = serializers.DateField()
|
||
cycle_weeks = serializers.IntegerField(min_value=1, max_value=52, default=1)
|
||
mode = serializers.ChoiceField(choices=WorkSchedule.Mode.choices, default=WorkSchedule.Mode.PRECISE)
|
||
day_targets = DayTargetInput(many=True, allow_empty=True, max_length=364, default=list)
|
||
week_targets = WeekTargetInput(many=True, allow_empty=True, max_length=52, default=list)
|
||
expected_revision = serializers.IntegerField(min_value=0)
|
||
slots = WeeklySlotInput(many=True, allow_empty=True, max_length=3640)
|
||
|
||
def validate(self, attrs):
|
||
checked_dates(attrs['valid_from'], None)
|
||
counts = {}
|
||
for slot in attrs['slots']:
|
||
if slot['week_index'] >= attrs['cycle_weeks']:
|
||
raise serializers.ValidationError('Une plage appartient à une semaine absente du cycle.')
|
||
key = (slot['week_index'], slot['weekday'])
|
||
counts[key] = counts.get(key, 0) + 1
|
||
if counts[key] > 10:
|
||
raise serializers.ValidationError('Une journée peut contenir au maximum 10 plages de travail.')
|
||
day_keys = set()
|
||
for target in attrs['day_targets']:
|
||
if target['week_index'] >= attrs['cycle_weeks']:
|
||
raise serializers.ValidationError({'day_targets': 'Un objectif journalier appartient à une semaine absente du cycle.'})
|
||
key = (target['week_index'], target['weekday'])
|
||
if key in day_keys:
|
||
raise serializers.ValidationError({'day_targets': 'Un jour ne peut avoir qu’un seul nombre d’heures prévu.'})
|
||
day_keys.add(key)
|
||
week_keys = set()
|
||
for target in attrs['week_targets']:
|
||
if target['week_index'] >= attrs['cycle_weeks']:
|
||
raise serializers.ValidationError({'week_targets': 'Un objectif hebdomadaire appartient à une semaine absente du cycle.'})
|
||
if target['week_index'] in week_keys:
|
||
raise serializers.ValidationError({'week_targets': 'Une semaine ne peut avoir qu’un seul total d’heures prévu.'})
|
||
week_keys.add(target['week_index'])
|
||
return attrs
|
||
|
||
|
||
class SlotSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = WorkScheduleSlot
|
||
fields = ['week_index', 'weekday', 'start_time', 'end_time', 'end_day_offset']
|
||
|
||
|
||
class ScheduleSerializer(serializers.ModelSerializer):
|
||
slots = SlotSerializer(many=True, read_only=True)
|
||
|
||
class Meta:
|
||
model = WorkSchedule
|
||
fields = ['id', 'user_id', 'valid_from', 'valid_to', 'cycle_weeks', 'mode', 'day_targets', 'week_targets', 'slots', 'updated_at']
|
||
|
||
|
||
class ExceptionSlotSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = ExceptionSlot
|
||
fields = ['start_time', 'end_time', 'end_day_offset']
|
||
|
||
|
||
class ExceptionSerializer(serializers.ModelSerializer):
|
||
slots = ExceptionSlotSerializer(many=True, read_only=True)
|
||
|
||
class Meta:
|
||
model = ScheduleException
|
||
fields = ['id', 'user_id', 'label', 'kind', 'mode', 'target_minutes', 'start_date', 'end_date', 'recurring', 'weekdays', 'every_weeks', 'slots', 'request_id']
|
||
|
||
|
||
class ExceptionInput(serializers.Serializer):
|
||
user_id = serializers.PrimaryKeyRelatedField(source='user', queryset=get_user_model().objects.filter(is_active=True))
|
||
expected_revision = serializers.IntegerField(min_value=0)
|
||
label = serializers.CharField(max_length=160, allow_blank=True, default='')
|
||
kind = serializers.ChoiceField(choices=ScheduleException.Kind.choices)
|
||
start_date = serializers.DateField()
|
||
end_date = serializers.DateField(allow_null=True, default=None)
|
||
recurring = serializers.BooleanField(default=False)
|
||
weekdays = serializers.ListField(child=serializers.IntegerField(min_value=0, max_value=6), max_length=7, default=list)
|
||
every_weeks = serializers.IntegerField(min_value=1, max_value=4, default=1)
|
||
slots = SlotInput(many=True, allow_empty=True, max_length=10, default=list)
|
||
|
||
def validate(self, attrs):
|
||
if not attrs['recurring'] and not attrs['end_date']:
|
||
attrs['end_date'] = attrs['start_date']
|
||
checked_dates(attrs['start_date'], attrs['end_date'])
|
||
if attrs['recurring'] and not attrs['weekdays']:
|
||
raise serializers.ValidationError({'weekdays': 'Choisissez au moins un jour de répétition.'})
|
||
attrs['weekdays'] = sorted(set(attrs['weekdays'])) if attrs['recurring'] else []
|
||
if attrs['kind'] == 'work' and not attrs['slots']:
|
||
raise serializers.ValidationError({'slots': 'Ajoutez au moins un créneau.'})
|
||
if attrs['kind'] != 'work' and attrs['slots']:
|
||
raise serializers.ValidationError({'slots': 'Un repos ou un congé ne comporte aucun créneau de travail.'})
|
||
return attrs
|
||
|
||
|
||
class RequestInput(serializers.Serializer):
|
||
expected_before = serializers.JSONField(required=False, write_only=True)
|
||
kind = serializers.ChoiceField(choices=DayChangeRequest.Kind.choices)
|
||
start_date = serializers.DateField()
|
||
end_date = serializers.DateField(required=False)
|
||
target_date = serializers.DateField(required=False, allow_null=True)
|
||
target_user_id = serializers.PrimaryKeyRelatedField(source='target_user', queryset=get_user_model().objects.filter(is_active=True), required=False, allow_null=True)
|
||
reason = serializers.CharField(max_length=2000, allow_blank=True, default='')
|
||
proposed_slots = SlotInput(many=True, allow_empty=True, max_length=10, default=list)
|
||
|
||
def validate(self, attrs):
|
||
if attrs['kind'] == 'leave':
|
||
require_permission(self.context['request'].user, 'request_leave')
|
||
attrs['end_date'] = attrs.get('end_date') or attrs['start_date']
|
||
checked_dates(attrs['start_date'], attrs['end_date'])
|
||
if (attrs['end_date'] - attrs['start_date']).days > 365:
|
||
raise serializers.ValidationError('Une demande peut couvrir au maximum 366 jours.')
|
||
if attrs['kind'] == 'swap':
|
||
if attrs['end_date'] != attrs['start_date']:
|
||
raise serializers.ValidationError('Un échange concerne une seule journée de votre planning.')
|
||
other = attrs.get('target_user')
|
||
if not other or other.pk == self.context['request'].user.pk:
|
||
raise serializers.ValidationError({'target_user_id': 'Choisissez un autre employé pour l’échange.'})
|
||
if attrs.get('target_date'):
|
||
checked_dates(attrs['target_date'], None)
|
||
attrs['target_user'] = other
|
||
elif attrs['kind'] == 'move':
|
||
if attrs['end_date'] != attrs['start_date'] or not attrs.get('target_date'):
|
||
raise serializers.ValidationError('Un déplacement concerne une journée de départ et une journée d’arrivée précises.')
|
||
checked_dates(attrs['target_date'], None)
|
||
if attrs['target_date'] == attrs['start_date']:
|
||
raise serializers.ValidationError({'target_date': 'Choisissez un autre jour pour déplacer ces heures.'})
|
||
if attrs.get('target_user'):
|
||
raise serializers.ValidationError({'target_user_id': 'Un déplacement reste sur votre propre planning.'})
|
||
elif attrs.get('target_date') or attrs.get('target_user'):
|
||
raise serializers.ValidationError('La seconde journée est réservée aux échanges et déplacements.')
|
||
if attrs['kind'] in ('hours', 'move') and not attrs['proposed_slots']:
|
||
raise serializers.ValidationError({'proposed_slots': 'Ajoutez les horaires souhaités.'})
|
||
if attrs['kind'] not in ('hours', 'move') and attrs['proposed_slots']:
|
||
raise serializers.ValidationError('Ces créneaux ne sont pas utilisés pour ce type de demande.')
|
||
# Le JSON stocké utilise les mêmes horaires locaux que le planning.
|
||
attrs['proposed_slots'] = [dict(start_time=s['start_time'].strftime('%H:%M'), end_time=s['end_time'].strftime('%H:%M'), end_day_offset=s['end_day_offset']) for s in attrs['proposed_slots']]
|
||
return attrs
|
||
|
||
|
||
class RequestSerializer(serializers.ModelSerializer):
|
||
user = NoteUserSerializer(read_only=True)
|
||
target_user = NoteUserSerializer(read_only=True)
|
||
reviewed_by = NoteUserSerializer(read_only=True)
|
||
|
||
class Meta:
|
||
model = DayChangeRequest
|
||
fields = ['id', 'user', 'kind', 'start_date', 'end_date', 'target_date', 'target_user', 'reason', 'proposed_slots', 'before', 'after', 'status', 'reviewed_by', 'reviewed_at', 'response', 'created_at', 'updated_at']
|
||
read_only_fields = fields
|