This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
calebasse/calebasse/agenda/models.py

662 lines
25 KiB
Python

# -*- coding: utf-8 -*-
from datetime import datetime, date, timedelta
from copy import copy
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.db import models
from django import forms
from calebasse.agenda import managers
from calebasse.utils import weeks_since_epoch, weekday_ranks
from calebasse.personnes.models import Holiday
from ..middleware.request import get_request
from interval import Interval
__all__ = (
'EventType',
'Event',
'EventWithAct',
)
class EventType(models.Model):
'''
Simple ``Event`` classifcation.
'''
class Meta:
verbose_name = u'Type d\'événement'
verbose_name_plural = u'Types d\'événement'
def __unicode__(self):
return self.label
label = models.CharField(_('label'), max_length=50)
rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
class Event(models.Model):
'''
Container model for general agenda events
'''
objects = managers.EventManager()
title = models.CharField(_('Title'), max_length=60, blank=True, default="")
description = models.TextField(_('Description'), max_length=100, blank=True, null=True)
event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
services = models.ManyToManyField('ressources.Service',
null=True, blank=True, default=None)
participants = models.ManyToManyField('personnes.People',
null=True, blank=True, default=None)
ressource = models.ForeignKey('ressources.Ressource', blank=True, null=True,
verbose_name=u'Ressource')
start_datetime = models.DateTimeField(_('Début'), db_index=True)
end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
old_ev_id = models.CharField(max_length=8, blank=True, null=True)
old_rr_id = models.CharField(max_length=8, blank=True, null=True)
# only used when there is no rr id
old_rs_id = models.CharField(max_length=8, blank=True, null=True)
# exception to is mutually exclusive with recurrence_periodicity
# an exception cannot be periodic
exception_to = models.ForeignKey('self', related_name='exceptions',
blank=True, null=True,
verbose_name=u'Exception à')
exception_date = models.DateField(blank=True, null=True,
verbose_name=u'Reporté du', db_index=True)
# canceled can only be used with exception to
canceled = models.BooleanField(_('Annulé'), db_index=True)
PERIODS = (
(1, u'Toutes les semaines'),
(2, u'Une semaine sur deux'),
(3, u'Une semaine sur trois'),
(4, 'Une semaine sur quatre'),
(5, 'Une semaine sur cinq')
)
OFFSET = range(0, 4)
PERIODICITIES = (
(1, u'Toutes les semaines'),
(2, u'Une semaine sur deux'),
(3, u'Une semaine sur trois'),
(4, u'Une semaine sur quatre'),
(5, u'Une semaine sur cinq'),
(6, u'La première semaine du mois'),
(7, u'La deuxième semaine du mois'),
(8, u'La troisième semaine du mois'),
(9, u'La quatrième semaine du mois'),
(13, u'La cinquième semaine du mois'),
(10, u'La dernière semaine du mois'),
(11, u'Les semaines paires'),
(12, u'Les semaines impaires')
)
WEEK_RANKS = (
(0, u'La première semaine du mois'),
(1, u'La deuxième semaine du mois'),
(2, u'La troisième semaine du mois'),
(3, u'La quatrième semaine du mois'),
(4, u'La cinquième semaine du mois'),
(-1, u'La dernière semaine du mois'),
# (-2, u'L\'avant dernière semaine du mois'),
# (-3, u'L\'antépénultième semaine du mois'),
# (-4, u'L\'anté-antépénultième semaine du mois'),
# (-5, u'L\'anté-anté-antépénultième semaine du mois')
)
PARITIES = (
(0, u'Les semaines paires'),
(1, u'Les semaines impaires')
)
recurrence_periodicity = models.PositiveIntegerField(
choices=PERIODICITIES,
verbose_name=u"Périodicité",
default=None,
blank=True,
null=True,
db_index=True)
recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
recurrence_week_offset = models.PositiveIntegerField(
choices=zip(OFFSET, OFFSET),
verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
default=0,
db_index=True)
recurrence_week_period = models.PositiveIntegerField(
choices=PERIODS,
verbose_name=u"Période en semaines",
default=None,
blank=True,
null=True,
db_index=True)
recurrence_week_rank = models.IntegerField(
verbose_name=u"Rang de la semaine dans le mois",
choices=WEEK_RANKS,
blank=True, null=True, db_index=True)
recurrence_week_parity = models.PositiveIntegerField(
choices=PARITIES,
verbose_name=u"Parité des semaines",
blank=True,
null=True,
db_index=True)
recurrence_end_date = models.DateField(
verbose_name=_(u'Fin de la récurrence'),
blank=True, null=True,
db_index=True)
PERIOD_LIST_TO_FIELDS = [(1, None, None),
(2, None, None),
(3, None, None),
(4, None, None),
(5, None, None),
(None, 0, None),
(None, 1, None),
(None, 2, None),
(None, 3, None),
(None, -1, None),
(None, None, 0),
(None, None, 1),
(None, 4, None),
]
class Meta:
verbose_name = u'Evénement'
verbose_name_plural = u'Evénements'
ordering = ('start_datetime', 'end_datetime', 'title')
unique_together = (('exception_to', 'exception_date'),)
def __init__(self, *args, **kwargs):
if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
super(Event, self).__init__(*args, **kwargs)
def clean(self):
'''Initialize recurrence fields if they are not.'''
self.sanitize()
if self.recurrence_periodicity:
if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
raise forms.ValidationError(u'La date de fin de périodicité doit être postérieure à la date de début.')
if self.recurrence_week_parity is not None:
if self.start_datetime:
week = self.start_datetime.date().isocalendar()[1]
start_week_parity = week % 2
if start_week_parity != self.recurrence_week_parity:
raise forms.ValidationError(u'La date de départ de la périodicité est en semaine {week}.'.format(week=week))
if self.recurrence_week_rank is not None and self.start_datetime:
start_week_ranks = weekday_ranks(self.start_datetime.date())
if self.recurrence_week_rank not in start_week_ranks:
raise forms.ValidationError('La date de début de périodicité doit faire partie de la bonne semaine dans le mois.')
def sanitize(self):
if self.recurrence_periodicity:
l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
else:
l = None, None, None
self.recurrence_week_period = l[0]
self.recurrence_week_rank = l[1]
self.recurrence_week_parity = l[2]
if self.start_datetime:
if self.recurrence_periodicity:
self.recurrence_week_day = self.start_datetime.weekday()
if self.recurrence_week_period is not None:
self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
def timedelta(self):
'''Distance between start and end of the event'''
return self.end_datetime - self.start_datetime
@property
def time(self):
return self.start_datetime.time()
@property
def date(self):
return self.start_datetime.date()
@property
def duration(self):
if self.timedelta():
return self.timedelta().seconds / 60
return 0
def match_date(self, date):
if self.is_recurring():
# consider exceptions
exception = self.get_exceptions_dict().get(date)
if exception is not None:
return exception if exception.match_date(date) else None
if self.canceled:
return None
if date.weekday() != self.recurrence_week_day:
return None
if self.start_datetime.date() > date:
return None
if self.recurrence_end_date and self.recurrence_end_date < date:
return None
if self.recurrence_week_period is not None:
if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
return None
elif self.recurrence_week_parity is not None:
if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
return None
elif self.recurrence_week_rank is not None:
if self.recurrence_week_rank not in weekday_ranks(date):
return None
else:
raise NotImplemented
return self
else:
return self if date == self.start_datetime.date() else None
def today_occurrence(self, today=None, match=False, upgrade=True):
'''For a recurring event compute the today 'Event'.
The computed event is the fake one that you cannot save to the database.
'''
today = today or date.today()
if self.canceled:
return None
if match:
exception = self.get_exceptions_dict().get(today)
if exception:
if exception.start_datetime.date() == today:
return exception.today_occurrence(today)
else:
return None
else:
exception_or_self = self.match_date(today)
if exception_or_self is None:
return None
if exception_or_self != self:
return exception_or_self.today_occurrence(today)
if self.event_type_id == 1 and type(self) != EventWithAct and upgrade:
self = self.eventwithact
if self.recurrence_periodicity is None:
return self
start_datetime = datetime.combine(today, self.start_datetime.timetz())
end_datetime = start_datetime + self.timedelta()
event = copy(self)
event.exception_to = self
event.exception_date = today
event.start_datetime = start_datetime
event.end_datetime = end_datetime
event.recurrence_periodicity = None
event.recurrence_week_offset = 0
event.recurrence_week_period = None
event.recurrence_week_parity = None
event.recurrence_week_rank = None
event.recurrence_end_date = None
event.parent = self
# the returned event is "virtual", it must not be saved
old_save = event.save
old_participants = list(self.participants.all())
old_services = list(self.services.all())
def save(*args, **kwargs):
event.id = None
event.event_ptr_id = None
old_save(*args, **kwargs)
if hasattr(self, 'exceptions_dict'):
self.exceptions_dict[event.start_datetime.date()] = event
event.services = old_services
event.participants = old_participants
event.save = old_save
event.save = save
return event
def next_occurence(self, today=None):
'''Returns the next occurence after today.'''
today = today or date.today()
for occurence in self.all_occurences():
if occurence.start_datetime.date() > today:
return occurence
def is_recurring(self):
'''Is this event multiple ?'''
return self.recurrence_periodicity is not None
def get_exceptions_dict(self):
if not hasattr(self, 'exceptions_dict'):
self.exceptions_dict = dict()
if self.exception_to_id is None:
for exception in self.exceptions.all():
self.exceptions_dict[exception.exception_date] = exception
return self.exceptions_dict
def all_occurences(self, limit=90):
'''Returns all occurences of this event as virtual Event objects
limit - compute occurrences until limit days in the future
Default is to limit to 90 days.
'''
if self.recurrence_periodicity is not None:
day = self.start_datetime.date()
max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
occurrences = []
if self.recurrence_week_period is not None:
delta = timedelta(days=self.recurrence_week_period*7)
while day <= end_date:
occurrence = self.today_occurrence(day, True)
if occurrence is not None:
occurrences.append(occurrence)
day += delta
elif self.recurrence_week_parity is not None:
delta = timedelta(days=7)
while day <= end_date:
if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
occurrence = self.today_occurrence(day, True)
if occurrence is not None:
occurrences.append(occurrence)
day += delta
elif self.recurrence_week_rank is not None:
delta = timedelta(days=7)
while day <= end_date:
if self.recurrence_week_rank in weekday_ranks(day):
occurrence = self.today_occurrence(day, True)
if occurrence is not None:
occurrences.append(occurrence)
day += delta
for exception in self.exceptions.all():
if not exception.canceled:
if exception.exception_date != exception.start_datetime.date() or exception.exception_date > end_date:
occurrences.append(exception.eventwithact if exception.event_type_id == 1 else exception)
return sorted(occurrences, key=lambda o: o.start_datetime)
else:
return [self]
def save(self, *args, **kwargs):
assert self.recurrence_periodicity is None or self.exception_to is None
assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
assert self.start_datetime is not None
self.sanitize() # init periodicity fields
super(Event, self).save(*args, **kwargs)
self.events_cleaning()
self.acts_cleaning()
def delete(self, *args, **kwargs):
if not self.one_act_already_billed():
for exception in self.exceptions.all():
exception.delete()
self.canceled = True
if hasattr(self, 'eventwithact'):
# Needed
self.eventwithact.canceled = True
# save will clean acts
self.save(*args, **kwargs)
@property
def acts(self):
from ..actes.models import Act
return Act.objects.filter(
models.Q(parent_event=self) | \
models.Q(parent_event__exception_to=self))
def one_act_not_new(self):
'''
Return True if at least one act of the present event or an act
of one of its exceptions is not new.
'''
return True if [a for a in self.acts if not a.is_new()] else False
def last_act_not_new(self):
'''
Return True if at least one act of the present event or an act
of one of its exceptions is not new.
'''
act = None
for a in self.acts:
if not a.is_new():
if not act or a.date > act.date:
act = a
return act
def one_act_already_billed(self):
'''
Return True if at least one act of the present event or an act
of one of its exceptions is already billed.
'''
return self.acts.filter(already_billed=True).exists()
def one_act_is_billed(self):
'''
Return True if at least one act of the present event or an act
of one of its exceptions is billed.
'''
return self.acts.filter(is_billed=True).exists()
def events_cleaning(self):
"""
Delete exceptions out of bounds if not with an associated act
already billed.
"""
events = None
if self.recurrence_end_date:
events = Event.objects.filter(
models.Q(exception_to=self,
exception_date__lt=self.start_datetime.date()) | \
models.Q(exception_to=self,
exception_date__gt=self.recurrence_end_date))
else:
events = Event.objects.filter(exception_to=self,
exception_date__lt=self.start_datetime.date())
for event in events:
if hasattr(event, 'eventwithact'):
if not event.eventwithact.act.already_billed:
event.delete()
else:
event.delete()
def acts_cleaning(self):
# list of occurences may have changed
from ..actes.models import Act
if self.exception_to:
# maybe a new exception, so look for parent acts with same date
# as exception date
acts = Act.objects.filter(models.Q(parent_event=self)
|models.Q(parent_event=self.exception_to,
date=self.exception_date))
else:
acts = Act.objects.filter(parent_event=self)
acts = acts.prefetch_related('actvalidationstate_set')
if acts:
eventwithact = self.eventwithact
for act in acts:
occurrence = eventwithact.today_occurrence(act.date)
if occurrence:
occurrence.update_act(act)
else:
if not act.already_billed:
act.delete()
def to_interval(self):
return Interval(self.start_datetime, self.end_datetime)
def is_event_absence(self):
return False
def get_inactive_participants(self):
return self.participants.filter(worker__enabled=False)
def get_missing_participants(self):
missing_participants = []
for participant in self.participants.all():
holidays = None
worker = participant.worker
holidays = Holiday.objects.for_worker(worker) \
.for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
if holidays:
missing_participants.append(participant)
return missing_participants
RECURRENCE_DESCRIPTION = [
u'Tous les %s', #(1, None, None),
u'Un %s sur deux', #(2, None, None),
u'Un %s sur trois', #(3, None, None),
u'Un %s sur quatre', #(4, None, None),
u'Un %s sur cinq', #(5, None, None),
u'Le premier %s du mois', #(None, 0, None),
u'Le deuxième %s du mois', #(None, 1, None),
u'Le troisième %s du mois', #(None, 2, None),
u'Le quatrième %s du mois', #(None, 3, None),
u'Le dernier %s du mois', #(None, 4, None),
u'Les %s les semaines paires', #(None, None, 0),
u'Les %s les semaines impaires', #(None, None, 1)
]
WEEKDAY_DESRIPTION = [
u'lundi',
u'mardi',
u'mercredi',
u'jeudi',
u'vendredi',
u'samedi',
u'dimanche'
]
def recurrence_description(self):
'''Self description of this recurring event'''
if not self.recurrence_periodicity:
return None
parts = []
parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
% self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
if self.recurrence_end_date:
parts.append(u'du')
else:
parts.append(u'à partir du')
parts.append(self.start_datetime.strftime('%d/%m/%Y'))
if self.recurrence_end_date:
parts.append(u'au')
parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
return u' '.join(parts)
def is_absent(self):
try:
return self.eventwithact.is_absent()
except self.DoesNotExist:
return False
def __unicode__(self):
return self.title
def __repr__(self):
return '<Event: on {start_datetime} with {participants}'.format(
start_datetime=self.start_datetime,
participants=self.participants.all() if self.id else '<un-saved>')
class EventWithActManager(managers.EventManager):
def create_patient_appointment(self, creator, title, patient,
doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
ressource=None, periodicity=1, until=False):
appointment = self.create_event(creator=creator,
title=title,
event_type=EventType(id=1),
participants=doctors,
services=[service],
start_datetime=start_datetime,
end_datetime=end_datetime,
ressource=ressource,
periodicity=periodicity,
until=until,
act_type=act_type,
patient=patient)
return appointment
class EventWithAct(Event):
'''An event corresponding to an act.'''
objects = EventWithActManager()
act_type = models.ForeignKey('ressources.ActType',
verbose_name=u'Type d\'acte')
patient = models.ForeignKey('dossiers.PatientRecord')
convocation_sent = models.BooleanField(blank=True,
verbose_name=u'Convoqué', db_index=True)
@property
def act(self):
for act in self.act_set.all():
if act.date == self.start_datetime.date():
return act
return self.build_act()
def get_state(self):
act = self.act
if act.id:
return act.get_state()
return None
def is_absent(self):
act = self.act
if act.id:
return act.is_absent()
return False
def build_act(self):
from ..actes.models import Act, ActValidationState
act = Act()
self.init_act(act)
old_save = act.save
def save(*args, **kwargs):
act.save = old_save
old_save(*args, **kwargs)
act.doctors = (participant.worker for participant in self.participants.all())
last_validation_state = ActValidationState.objects.create(
act=act, state_name='NON_VALIDE',
author=self.creator, previous_state=None)
act.last_validation_state = last_validation_state
old_save(*args, **kwargs)
act.save = save
return act
def update_act(self, act):
'''Update an act to match details of the meeting'''
self.init_act(act)
act.save()
def init_act(self, act):
if not act.is_billed:
delta = self.timedelta()
duration = delta.seconds // 60
act._duration = duration
act.act_type = self.act_type
act.patient = self.patient
act.date = self.start_datetime.date()
act.time = self.start_datetime.time()
act.parent_event = self
def save(self, *args, **kwargs):
'''Force event_type to be patient meeting.'''
self.event_type = EventType(id=1)
super(EventWithAct, self).save(*args, **kwargs)
def is_event_absence(self):
return self.act.is_absent()
def __unicode__(self):
kwargs = {
'patient': self.patient,
'start_datetime': self.start_datetime,
'act_type': self.act_type
}
kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
'{act_type} ({act_type.id})'.format(**kwargs)
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
@receiver(m2m_changed, sender=Event.participants.through)
def participants_changed(sender, instance, action, **kwargs):
if action.startswith('post'):
workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
for act in instance.act_set.all():
act.doctors = workers