Projet

Général

Profil

Télécharger (25,2 ko) Statistiques
| Branche: | Tag: | Révision:

calebasse / calebasse / agenda / models.py @ 1164bc89

1 96f613c6 Benjamin Dauvergne
# -*- coding: utf-8 -*-
2 ba34dc24 Jérôme Schneider
3 76974b6f Benjamin Dauvergne
from datetime import datetime, date, timedelta
4
from copy import copy
5 ba34dc24 Jérôme Schneider
6
from django.utils.translation import ugettext_lazy as _
7 76974b6f Benjamin Dauvergne
from django.contrib.auth.models import User
8 ba34dc24 Jérôme Schneider
from django.db import models
9 4ce756c8 Benjamin Dauvergne
from django import forms
10 ba34dc24 Jérôme Schneider
11 11984ab3 Jérôme Schneider
from calebasse.agenda import managers
12 4ce756c8 Benjamin Dauvergne
from calebasse.utils import weeks_since_epoch, weekday_ranks
13 e6b125bd Mikaël Ates
from calebasse.personnes.models import Holiday
14 a01d85be Serghei MIHAI
15
from ..middleware.request import get_request
16
17 7a0a30d7 Benjamin Dauvergne
from interval import Interval
18 ba34dc24 Jérôme Schneider
19
__all__ = (
20
    'EventType',
21
    'Event',
22 76974b6f Benjamin Dauvergne
    'EventWithAct',
23 ba34dc24 Jérôme Schneider
)
24
25
class EventType(models.Model):
26
    '''
27
    Simple ``Event`` classifcation.
28
    '''
29
    class Meta:
30 e39efb4a Jérôme Schneider
        verbose_name = u'Type d\'événement'
31
        verbose_name_plural = u'Types d\'événement'
32 ba34dc24 Jérôme Schneider
33
    def __unicode__(self):
34
        return self.label
35
36 e39efb4a Jérôme Schneider
    label = models.CharField(_('label'), max_length=50)
37 68e9f8c1 Frédéric Péters
    rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
38 e39efb4a Jérôme Schneider
39 84091f2c Jérôme Schneider
class Event(models.Model):
40 ba34dc24 Jérôme Schneider
    '''
41 76974b6f Benjamin Dauvergne
    Container model for general agenda events
42 ba34dc24 Jérôme Schneider
    '''
43 e39efb4a Jérôme Schneider
    objects = managers.EventManager()
44 ba34dc24 Jérôme Schneider
45 e90f4c73 Jérôme Schneider
    title = models.CharField(_('Title'), max_length=60, blank=True, default="")
46 a334f1e6 Mikaël Ates
    description = models.TextField(_('Description'), max_length=100, blank=True, null=True)
47 69a9009e Jérôme Schneider
    event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
48 76974b6f Benjamin Dauvergne
    creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
49
    create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
50 ba34dc24 Jérôme Schneider
51 84091f2c Jérôme Schneider
    services = models.ManyToManyField('ressources.Service',
52
            null=True, blank=True, default=None)
53 e39efb4a Jérôme Schneider
    participants = models.ManyToManyField('personnes.People',
54
            null=True, blank=True, default=None)
55 dd986559 Serghei MIHAI
    ressource = models.ForeignKey('ressources.Ressource', blank=True, null=True,
56
                                  verbose_name=u'Ressource')
57 e39efb4a Jérôme Schneider
58 5c93596e Benjamin Dauvergne
    start_datetime = models.DateTimeField(_('Début'), db_index=True)
59 76974b6f Benjamin Dauvergne
    end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
60 74102956 Benjamin Dauvergne
    old_ev_id = models.CharField(max_length=8, blank=True, null=True)
61
    old_rr_id = models.CharField(max_length=8, blank=True, null=True)
62 44482a62 Benjamin Dauvergne
    # only used when there is no rr id
63 74102956 Benjamin Dauvergne
    old_rs_id = models.CharField(max_length=8, blank=True, null=True)
64 a04356e2 Benjamin Dauvergne
    # exception to is mutually exclusive with recurrence_periodicity
65
    # an exception cannot be periodic
66
    exception_to = models.ForeignKey('self', related_name='exceptions',
67
            blank=True, null=True,
68
            verbose_name=u'Exception à')
69
    exception_date = models.DateField(blank=True, null=True,
70 e9fb7b88 Benjamin Dauvergne
            verbose_name=u'Reporté du', db_index=True)
71 a04356e2 Benjamin Dauvergne
    # canceled can only be used with exception to
72 1164bc89 Mikaël Ates
    canceled = models.BooleanField(_('Annulé'), db_index=True, default=False)
73 76974b6f Benjamin Dauvergne
74
    PERIODS = (
75
            (1, u'Toutes les semaines'),
76
            (2, u'Une semaine sur deux'),
77
            (3, u'Une semaine sur trois'),
78
            (4, 'Une semaine sur quatre'),
79
            (5, 'Une semaine sur cinq')
80
    )
81 3c5df84d Jérôme Schneider
    OFFSET = range(0, 4)
82 4ce756c8 Benjamin Dauvergne
    PERIODICITIES = (
83
            (1, u'Toutes les semaines'),
84
            (2, u'Une semaine sur deux'),
85
            (3, u'Une semaine sur trois'),
86
            (4, u'Une semaine sur quatre'),
87
            (5, u'Une semaine sur cinq'),
88
            (6, u'La première semaine du mois'),
89
            (7, u'La deuxième semaine du mois'),
90
            (8, u'La troisième semaine du mois'),
91
            (9, u'La quatrième semaine du mois'),
92 9de67cc5 Benjamin Dauvergne
            (13, u'La cinquième semaine du mois'),
93 4ce756c8 Benjamin Dauvergne
            (10, u'La dernière semaine du mois'),
94
            (11, u'Les semaines paires'),
95
            (12, u'Les semaines impaires')
96
    )
97
    WEEK_RANKS = (
98
            (0, u'La première semaine du mois'),
99
            (1, u'La deuxième semaine du mois'),
100
            (2, u'La troisième semaine du mois'),
101
            (3, u'La quatrième semaine du mois'),
102 9433e992 Benjamin Dauvergne
            (4, u'La cinquième semaine du mois'),
103
            (-1, u'La dernière semaine du mois'),
104
#            (-2, u'L\'avant dernière semaine du mois'),
105
#            (-3, u'L\'antépénultième semaine du mois'),
106
#            (-4, u'L\'anté-antépénultième semaine du mois'),
107
#            (-5, u'L\'anté-anté-antépénultième semaine du mois')
108
109 4ce756c8 Benjamin Dauvergne
    )
110
    PARITIES = (
111
            (0, u'Les semaines paires'),
112
            (1, u'Les semaines impaires')
113
    )
114
    recurrence_periodicity = models.PositiveIntegerField(
115
            choices=PERIODICITIES,
116
            verbose_name=u"Périodicité",
117
            default=None,
118
            blank=True,
119 e9fb7b88 Benjamin Dauvergne
            null=True,
120
            db_index=True)
121
    recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
122 76974b6f Benjamin Dauvergne
    recurrence_week_offset = models.PositiveIntegerField(
123
            choices=zip(OFFSET, OFFSET),
124
            verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
125
            default=0,
126
            db_index=True)
127
    recurrence_week_period = models.PositiveIntegerField(
128
            choices=PERIODS,
129
            verbose_name=u"Période en semaines",
130
            default=None,
131
            blank=True,
132
            null=True,
133
            db_index=True)
134 9433e992 Benjamin Dauvergne
    recurrence_week_rank = models.IntegerField(
135 4ce756c8 Benjamin Dauvergne
            verbose_name=u"Rang de la semaine dans le mois",
136
            choices=WEEK_RANKS,
137 e9fb7b88 Benjamin Dauvergne
            blank=True, null=True, db_index=True)
138 4ce756c8 Benjamin Dauvergne
    recurrence_week_parity = models.PositiveIntegerField(
139
            choices=PARITIES,
140
            verbose_name=u"Parité des semaines",
141
            blank=True,
142 e9fb7b88 Benjamin Dauvergne
            null=True,
143
            db_index=True)
144 76974b6f Benjamin Dauvergne
    recurrence_end_date = models.DateField(
145
            verbose_name=_(u'Fin de la récurrence'),
146
            blank=True, null=True,
147
            db_index=True)
148
149 4ce756c8 Benjamin Dauvergne
    PERIOD_LIST_TO_FIELDS = [(1, None, None),
150
        (2, None, None),
151
        (3, None, None),
152
        (4, None, None),
153
        (5, None, None),
154
        (None, 0, None),
155
        (None, 1, None),
156
        (None, 2, None),
157
        (None, 3, None),
158 9de67cc5 Benjamin Dauvergne
        (None, -1, None),
159 4ce756c8 Benjamin Dauvergne
        (None, None, 0),
160 9de67cc5 Benjamin Dauvergne
        (None, None, 1),
161
        (None, 4, None),
162 4ce756c8 Benjamin Dauvergne
    ]
163
164 ba34dc24 Jérôme Schneider
    class Meta:
165 e39efb4a Jérôme Schneider
        verbose_name = u'Evénement'
166
        verbose_name_plural = u'Evénements'
167 76974b6f Benjamin Dauvergne
        ordering = ('start_datetime', 'end_datetime', 'title')
168 a04356e2 Benjamin Dauvergne
        unique_together = (('exception_to', 'exception_date'),)
169 76974b6f Benjamin Dauvergne
170
    def __init__(self, *args, **kwargs):
171
        if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
172
            kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
173
        super(Event, self).__init__(*args, **kwargs)
174
175
    def clean(self):
176
        '''Initialize recurrence fields if they are not.'''
177 f16de572 Benjamin Dauvergne
        self.sanitize()
178 4ce756c8 Benjamin Dauvergne
        if self.recurrence_periodicity:
179 f16de572 Benjamin Dauvergne
            if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
180 4ce756c8 Benjamin Dauvergne
                raise forms.ValidationError(u'La date de fin de périodicité doit être postérieure à la date de début.')
181
        if self.recurrence_week_parity is not None:
182
            if self.start_datetime:
183
                week = self.start_datetime.date().isocalendar()[1]
184
                start_week_parity = week % 2
185
                if start_week_parity != self.recurrence_week_parity:
186 05cf3715 Frédéric Péters
                    raise forms.ValidationError(u'La date de départ de la périodicité est en semaine {week}.'.format(week=week))
187 4ce756c8 Benjamin Dauvergne
        if self.recurrence_week_rank is not None and self.start_datetime:
188
            start_week_ranks = weekday_ranks(self.start_datetime.date())
189
            if self.recurrence_week_rank not in start_week_ranks:
190
                raise forms.ValidationError('La date de début de périodicité doit faire partie de la bonne semaine dans le mois.')
191 76974b6f Benjamin Dauvergne
192 f16de572 Benjamin Dauvergne
    def sanitize(self):
193
        if self.recurrence_periodicity:
194
            l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
195
        else:
196
            l = None, None, None
197
        self.recurrence_week_period = l[0]
198
        self.recurrence_week_rank = l[1]
199
        self.recurrence_week_parity = l[2]
200
        if self.start_datetime:
201
            if self.recurrence_periodicity:
202
                self.recurrence_week_day = self.start_datetime.weekday()
203
            if self.recurrence_week_period is not None:
204
                self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
205
206 76974b6f Benjamin Dauvergne
    def timedelta(self):
207
        '''Distance between start and end of the event'''
208
        return self.end_datetime - self.start_datetime
209
210 b2ab5f4c Mikaël Ates
    @property
211
    def time(self):
212
        return self.start_datetime.time()
213
214
    @property
215
    def date(self):
216
        return self.start_datetime.date()
217
218
    @property
219
    def duration(self):
220
        if self.timedelta():
221
            return self.timedelta().seconds / 60
222
        return 0
223
224 a04356e2 Benjamin Dauvergne
    def match_date(self, date):
225
        if self.is_recurring():
226
            # consider exceptions
227
            exception = self.get_exceptions_dict().get(date)
228
            if exception is not None:
229
                return exception if exception.match_date(date) else None
230
            if self.canceled:
231
                return None
232
            if date.weekday() != self.recurrence_week_day:
233
                return None
234
            if self.start_datetime.date() > date:
235
                return None
236
            if self.recurrence_end_date and self.recurrence_end_date < date:
237
                return None
238
            if self.recurrence_week_period is not None:
239
                if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
240
                    return None
241
            elif self.recurrence_week_parity is not None:
242
                if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
243
                    return None
244
            elif self.recurrence_week_rank is not None:
245
                if self.recurrence_week_rank not in weekday_ranks(date):
246
                    return None
247
            else:
248
                raise NotImplemented
249
            return self
250
        else:
251 ced57915 Benjamin Dauvergne
            return self if date == self.start_datetime.date() else None
252 a04356e2 Benjamin Dauvergne
253
254 b8a9f824 Benjamin Dauvergne
    def today_occurrence(self, today=None, match=False, upgrade=True):
255 76974b6f Benjamin Dauvergne
        '''For a recurring event compute the today 'Event'.
256
257
           The computed event is the fake one that you cannot save to the database.
258 ba34dc24 Jérôme Schneider
        '''
259 76974b6f Benjamin Dauvergne
        today = today or date.today()
260 a04356e2 Benjamin Dauvergne
        if self.canceled:
261 76974b6f Benjamin Dauvergne
            return None
262 a04356e2 Benjamin Dauvergne
        if match:
263
            exception = self.get_exceptions_dict().get(today)
264 b16fae0e Benjamin Dauvergne
            if exception:
265
                if exception.start_datetime.date() == today:
266
                    return exception.today_occurrence(today)
267
                else:
268
                    return None
269 4ce756c8 Benjamin Dauvergne
        else:
270 a04356e2 Benjamin Dauvergne
            exception_or_self = self.match_date(today)
271
            if exception_or_self is None:
272
                return None
273
            if exception_or_self != self:
274 5c2d0ea2 Benjamin Dauvergne
                return exception_or_self.today_occurrence(today)
275 b8a9f824 Benjamin Dauvergne
        if self.event_type_id == 1 and type(self) != EventWithAct and upgrade:
276
           self = self.eventwithact
277 59136255 Benjamin Dauvergne
        if self.recurrence_periodicity is None:
278
            return self
279 76974b6f Benjamin Dauvergne
        start_datetime = datetime.combine(today, self.start_datetime.timetz())
280
        end_datetime = start_datetime + self.timedelta()
281
        event = copy(self)
282 a04356e2 Benjamin Dauvergne
        event.exception_to = self
283
        event.exception_date = today
284 76974b6f Benjamin Dauvergne
        event.start_datetime = start_datetime
285
        event.end_datetime = end_datetime
286 a04356e2 Benjamin Dauvergne
        event.recurrence_periodicity = None
287
        event.recurrence_week_offset = 0
288 76974b6f Benjamin Dauvergne
        event.recurrence_week_period = None
289 a04356e2 Benjamin Dauvergne
        event.recurrence_week_parity = None
290
        event.recurrence_week_rank = None
291
        event.recurrence_end_date = None
292 76974b6f Benjamin Dauvergne
        event.parent = self
293 4ce756c8 Benjamin Dauvergne
        # the returned event is "virtual", it must not be saved
294 a04356e2 Benjamin Dauvergne
        old_save = event.save
295
        old_participants = list(self.participants.all())
296 33522bec Benjamin Dauvergne
        old_services = list(self.services.all())
297 a334f1e6 Mikaël Ates
        def save(*args, **kwargs):
298 a04356e2 Benjamin Dauvergne
            event.id = None
299 86d19877 Benjamin Dauvergne
            event.event_ptr_id = None
300 a04356e2 Benjamin Dauvergne
            old_save(*args, **kwargs)
301 dbe774de Benjamin Dauvergne
            if hasattr(self, 'exceptions_dict'):
302
                self.exceptions_dict[event.start_datetime.date()] = event
303 33522bec Benjamin Dauvergne
            event.services = old_services
304 a04356e2 Benjamin Dauvergne
            event.participants = old_participants
305 083e9b50 Benjamin Dauvergne
            event.save = old_save
306 76974b6f Benjamin Dauvergne
        event.save = save
307
        return event
308
309
    def next_occurence(self, today=None):
310
        '''Returns the next occurence after today.'''
311
        today = today or date.today()
312
        for occurence in self.all_occurences():
313
            if occurence.start_datetime.date() > today:
314
                return occurence
315
316
    def is_recurring(self):
317
        '''Is this event multiple ?'''
318 4ce756c8 Benjamin Dauvergne
        return self.recurrence_periodicity is not None
319 76974b6f Benjamin Dauvergne
320 a04356e2 Benjamin Dauvergne
    def get_exceptions_dict(self):
321
        if not hasattr(self, 'exceptions_dict'):
322
            self.exceptions_dict = dict()
323 508c3726 Benjamin Dauvergne
            if self.exception_to_id is None:
324 b8a9f824 Benjamin Dauvergne
                for exception in self.exceptions.all():
325 508c3726 Benjamin Dauvergne
                    self.exceptions_dict[exception.exception_date] = exception
326 a04356e2 Benjamin Dauvergne
        return self.exceptions_dict
327
328 76974b6f Benjamin Dauvergne
    def all_occurences(self, limit=90):
329 4ce756c8 Benjamin Dauvergne
        '''Returns all occurences of this event as virtual Event objects
330 76974b6f Benjamin Dauvergne
331
           limit - compute occurrences until limit days in the future
332
333
           Default is to limit to 90 days.
334 ba34dc24 Jérôme Schneider
        '''
335 4ce756c8 Benjamin Dauvergne
        if self.recurrence_periodicity is not None:
336 76974b6f Benjamin Dauvergne
            day = self.start_datetime.date()
337
            max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
338
            end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
339 a04356e2 Benjamin Dauvergne
            occurrences = []
340 4ce756c8 Benjamin Dauvergne
            if self.recurrence_week_period is not None:
341
                delta = timedelta(days=self.recurrence_week_period*7)
342
                while day <= end_date:
343 790fd669 Benjamin Dauvergne
                    occurrence = self.today_occurrence(day, True)
344 a04356e2 Benjamin Dauvergne
                    if occurrence is not None:
345
                        occurrences.append(occurrence)
346 4ce756c8 Benjamin Dauvergne
                    day += delta
347
            elif self.recurrence_week_parity is not None:
348
                delta = timedelta(days=7)
349
                while day <= end_date:
350
                    if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
351 790fd669 Benjamin Dauvergne
                        occurrence = self.today_occurrence(day, True)
352 a04356e2 Benjamin Dauvergne
                        if occurrence is not None:
353
                            occurrences.append(occurrence)
354 4ce756c8 Benjamin Dauvergne
                    day += delta
355
            elif self.recurrence_week_rank is not None:
356
                delta = timedelta(days=7)
357
                while day <= end_date:
358
                    if self.recurrence_week_rank in weekday_ranks(day):
359 790fd669 Benjamin Dauvergne
                        occurrence = self.today_occurrence(day, True)
360 a04356e2 Benjamin Dauvergne
                        if occurrence is not None:
361
                            occurrences.append(occurrence)
362 4ce756c8 Benjamin Dauvergne
                    day += delta
363 a04356e2 Benjamin Dauvergne
            for exception in self.exceptions.all():
364 fcc58fae Jérôme Schneider
                if not exception.canceled:
365
                    if exception.exception_date != exception.start_datetime.date() or exception.exception_date > end_date:
366
                        occurrences.append(exception.eventwithact if exception.event_type_id == 1 else exception)
367 a04356e2 Benjamin Dauvergne
            return sorted(occurrences, key=lambda o: o.start_datetime)
368 ba34dc24 Jérôme Schneider
        else:
369 a04356e2 Benjamin Dauvergne
            return [self]
370 ba34dc24 Jérôme Schneider
371 4ce756c8 Benjamin Dauvergne
    def save(self, *args, **kwargs):
372 0acdd88a Benjamin Dauvergne
        assert self.recurrence_periodicity is None or self.exception_to is None
373 12a8ef2b Benjamin Dauvergne
        assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
374
        assert self.start_datetime is not None
375 f16de572 Benjamin Dauvergne
        self.sanitize() # init periodicity fields
376 4ce756c8 Benjamin Dauvergne
        super(Event, self).save(*args, **kwargs)
377 b2ab5f4c Mikaël Ates
        self.events_cleaning()
378 5f3ce603 Benjamin Dauvergne
        self.acts_cleaning()
379 4ce756c8 Benjamin Dauvergne
380 fcd786f5 Benjamin Dauvergne
    def delete(self, *args, **kwargs):
381 b2ab5f4c Mikaël Ates
        if not self.one_act_already_billed():
382
            for exception in self.exceptions.all():
383
                exception.delete()
384
            self.canceled = True
385
            if hasattr(self, 'eventwithact'):
386
                # Needed
387
                self.eventwithact.canceled = True
388
            # save will clean acts
389
            self.save(*args, **kwargs)
390
391
    @property
392
    def acts(self):
393
        from ..actes.models import Act
394
        return Act.objects.filter(
395
                models.Q(parent_event=self) | \
396
                models.Q(parent_event__exception_to=self))
397
398 a2e49cca Mikaël Ates
    def one_act_not_new(self):
399
        '''
400
            Return True if at least one act of the present event or an act
401
            of one of its exceptions is not new.
402
        '''
403
        return True if [a for a in self.acts if not a.is_new()] else False
404
405
    def last_act_not_new(self):
406
        '''
407
            Return True if at least one act of the present event or an act
408
            of one of its exceptions is not new.
409
        '''
410
        act = None
411
        for a in self.acts:
412
            if not a.is_new():
413
                if not act or a.date > act.date:
414
                    act = a
415
        return act
416
417 b2ab5f4c Mikaël Ates
    def one_act_already_billed(self):
418
        '''
419
            Return True if at least one act of the present event or an act
420
            of one of its exceptions is already billed.
421
        '''
422
        return self.acts.filter(already_billed=True).exists()
423
424
    def one_act_is_billed(self):
425
        '''
426
            Return True if at least one act of the present event or an act
427
            of one of its exceptions is billed.
428
        '''
429
        return self.acts.filter(is_billed=True).exists()
430
431
    def events_cleaning(self):
432
        """
433
            Delete exceptions out of bounds if not with an associated act
434
            already billed.
435
        """
436
        events = None
437
        if self.recurrence_end_date:
438
            events = Event.objects.filter(
439
                models.Q(exception_to=self,
440
                    exception_date__lt=self.start_datetime.date()) | \
441
                models.Q(exception_to=self,
442
                    exception_date__gt=self.recurrence_end_date))
443
        else:
444
            events = Event.objects.filter(exception_to=self,
445
                    exception_date__lt=self.start_datetime.date())
446
        for event in events:
447
            if hasattr(event, 'eventwithact'):
448
                if not event.eventwithact.act.already_billed:
449
                    event.delete()
450
            else:
451
                event.delete()
452 5f3ce603 Benjamin Dauvergne
453
    def acts_cleaning(self):
454
        # list of occurences may have changed
455
        from ..actes.models import Act
456
        if self.exception_to:
457
            # maybe a new exception, so look for parent acts with same date
458
            # as exception date
459
            acts = Act.objects.filter(models.Q(parent_event=self)
460
                    |models.Q(parent_event=self.exception_to,
461
                        date=self.exception_date))
462
        else:
463
            acts = Act.objects.filter(parent_event=self)
464
        acts = acts.prefetch_related('actvalidationstate_set')
465
        if acts:
466
            eventwithact = self.eventwithact
467
            for act in acts:
468 1d10b2c6 Benjamin Dauvergne
                occurrence = eventwithact.today_occurrence(act.date)
469
                if occurrence:
470
                    occurrence.update_act(act)
471
                else:
472 b2ab5f4c Mikaël Ates
                    if not act.already_billed:
473
                        act.delete()
474 fcd786f5 Benjamin Dauvergne
475 76974b6f Benjamin Dauvergne
    def to_interval(self):
476
        return Interval(self.start_datetime, self.end_datetime)
477 ba34dc24 Jérôme Schneider
478 76974b6f Benjamin Dauvergne
    def is_event_absence(self):
479
        return False
480 ba34dc24 Jérôme Schneider
481 bba85766 Serghei MIHAI
    def get_inactive_participants(self):
482
        return self.participants.filter(worker__enabled=False)
483
484 e6b125bd Mikaël Ates
    def get_missing_participants(self):
485
        missing_participants = []
486
        for participant in self.participants.all():
487
            holidays = None
488
            worker = participant.worker
489
            holidays = Holiday.objects.for_worker(worker) \
490
                  .for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
491
            if holidays:
492
                missing_participants.append(participant)
493
        return missing_participants
494
495 3e9b47e9 Benjamin Dauvergne
    RECURRENCE_DESCRIPTION = [
496
            u'Tous les %s',      #(1, None, None),
497
            u'Un %s sur deux',   #(2, None, None),
498
            u'Un %s sur trois',  #(3, None, None),
499
            u'Un %s sur quatre', #(4, None, None),
500
            u'Un %s sur cinq',   #(5, None, None),
501
            u'Le premier %s du mois',   #(None, 0, None),
502
            u'Le deuxième %s du mois',  #(None, 1, None),
503
            u'Le troisième %s du mois', #(None, 2, None),
504
            u'Le quatrième %s du mois', #(None, 3, None),
505
            u'Le dernier %s du mois',   #(None, 4, None),
506
            u'Les %s les semaines paires',    #(None, None, 0),
507
            u'Les %s les semaines impaires', #(None, None, 1)
508
    ]
509
510
    WEEKDAY_DESRIPTION = [
511
            u'lundi',
512
            u'mardi',
513
            u'mercredi',
514
            u'jeudi',
515
            u'vendredi',
516
            u'samedi',
517
            u'dimanche'
518
    ]
519
520
    def recurrence_description(self):
521
        '''Self description of this recurring event'''
522
        if not self.recurrence_periodicity:
523
            return None
524
        parts = []
525 9ccdb2fe Benjamin Dauvergne
        parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
526 3e9b47e9 Benjamin Dauvergne
            % self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
527
        if self.recurrence_end_date:
528
            parts.append(u'du')
529
        else:
530
            parts.append(u'à partir du')
531
        parts.append(self.start_datetime.strftime('%d/%m/%Y'))
532
        if self.recurrence_end_date:
533
            parts.append(u'au')
534
            parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
535
        return u' '.join(parts)
536
537 7a2fc3bb Serghei MIHAI
    def is_absent(self):
538
        try:
539
            return self.eventwithact.is_absent()
540
        except self.DoesNotExist:
541
            return False
542
543 ba34dc24 Jérôme Schneider
    def __unicode__(self):
544 76974b6f Benjamin Dauvergne
        return self.title
545 ba34dc24 Jérôme Schneider
546 6190a8ce Benjamin Dauvergne
    def __repr__(self):
547
        return '<Event: on {start_datetime} with {participants}'.format(
548
                start_datetime=self.start_datetime,
549
                participants=self.participants.all() if self.id else '<un-saved>')
550
551 ba34dc24 Jérôme Schneider
552 76974b6f Benjamin Dauvergne
class EventWithActManager(managers.EventManager):
553
    def create_patient_appointment(self, creator, title, patient,
554
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
555 dd986559 Serghei MIHAI
            ressource=None, periodicity=1, until=False):
556 76974b6f Benjamin Dauvergne
        appointment = self.create_event(creator=creator,
557
                title=title,
558
                event_type=EventType(id=1),
559
                participants=doctors,
560
                services=[service],
561
                start_datetime=start_datetime,
562
                end_datetime=end_datetime,
563 dd986559 Serghei MIHAI
                ressource=ressource,
564 76974b6f Benjamin Dauvergne
                periodicity=periodicity,
565
                until=until,
566
                act_type=act_type,
567
                patient=patient)
568
        return appointment
569
570
571
class EventWithAct(Event):
572
    '''An event corresponding to an act.'''
573
    objects = EventWithActManager()
574
    act_type = models.ForeignKey('ressources.ActType',
575
        verbose_name=u'Type d\'acte')
576
    patient = models.ForeignKey('dossiers.PatientRecord')
577 ddcbee95 Benjamin Dauvergne
    convocation_sent = models.BooleanField(blank=True,
578 1164bc89 Mikaël Ates
        verbose_name=u'Convoqué', db_index=True, default=False)
579 ba34dc24 Jérôme Schneider
580 5c2d0ea2 Benjamin Dauvergne
581 ba34dc24 Jérôme Schneider
    @property
582 76974b6f Benjamin Dauvergne
    def act(self):
583 6b0ae820 Benjamin Dauvergne
        for act in self.act_set.all():
584
            if act.date == self.start_datetime.date():
585
                return act
586 433b67bb Benjamin Dauvergne
        return self.build_act()
587 76974b6f Benjamin Dauvergne
588 433b67bb Benjamin Dauvergne
    def get_state(self):
589
        act = self.act
590
        if act.id:
591
            return act.get_state()
592
        return None
593
594 4b5bbcb1 Mikaël Ates
    def is_absent(self):
595
        act = self.act
596
        if act.id:
597
            return act.is_absent()
598
        return False
599
600 433b67bb Benjamin Dauvergne
    def build_act(self):
601 76974b6f Benjamin Dauvergne
        from ..actes.models import Act, ActValidationState
602 433b67bb Benjamin Dauvergne
        act = Act()
603
        self.init_act(act)
604
        old_save = act.save
605
        def save(*args, **kwargs):
606 8fa0047b Benjamin Dauvergne
            act.save = old_save
607 433b67bb Benjamin Dauvergne
            old_save(*args, **kwargs)
608 d3cfbd19 Serghei MIHAI
            act.doctors = (participant.worker for participant in self.participants.all())
609 981abc28 Jérôme Schneider
            last_validation_state = ActValidationState.objects.create(
610
                    act=act, state_name='NON_VALIDE',
611
                    author=self.creator, previous_state=None)
612
            act.last_validation_state = last_validation_state
613 32551648 Jérôme Schneider
            old_save(*args, **kwargs)
614 433b67bb Benjamin Dauvergne
        act.save = save
615 76974b6f Benjamin Dauvergne
        return act
616
617
    def update_act(self, act):
618
        '''Update an act to match details of the meeting'''
619 433b67bb Benjamin Dauvergne
        self.init_act(act)
620
        act.save()
621
622
    def init_act(self, act):
623 b2ab5f4c Mikaël Ates
        if not act.is_billed:
624
            delta = self.timedelta()
625
            duration = delta.seconds // 60
626
            act._duration = duration
627
            act.act_type = self.act_type
628
            act.patient = self.patient
629
            act.date = self.start_datetime.date()
630
            act.time = self.start_datetime.time()
631 046a69ee Benjamin Dauvergne
        act.parent_event = self
632 76974b6f Benjamin Dauvergne
633
    def save(self, *args, **kwargs):
634
        '''Force event_type to be patient meeting.'''
635
        self.event_type = EventType(id=1)
636
        super(EventWithAct, self).save(*args, **kwargs)
637 ba0e06ef Benjamin Dauvergne
638 5f4c5dd2 Frédéric Péters
    def is_event_absence(self):
639 76974b6f Benjamin Dauvergne
        return self.act.is_absent()
640
641
    def __unicode__(self):
642
        kwargs = {
643
                'patient': self.patient,
644
                'start_datetime': self.start_datetime,
645
                'act_type': self.act_type
646
        }
647
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
648
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
649
            '{act_type} ({act_type.id})'.format(**kwargs)
650 6eff18f3 Benjamin Dauvergne
651
652
from django.db.models.signals import m2m_changed
653
from django.dispatch import receiver
654
655
656
@receiver(m2m_changed, sender=Event.participants.through)
657
def participants_changed(sender, instance, action, **kwargs):
658
    if action.startswith('post'):
659
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
660
        for act in instance.act_set.all():
661
            act.doctors = workers