Louis Mazin 4db822aebe big maj
2026-10-05 17:52:28 +02:00

195 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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