Project

General

Profile

Download (22.5 KB) Statistics
| Branch: | Tag: | Revision:

calebasse / calebasse / agenda / models.py @ 784abc51

1
# -*- coding: utf-8 -*-
2

    
3
from datetime import datetime, date, timedelta
4
from copy import copy
5

    
6
from django.utils.translation import ugettext_lazy as _
7
from django.contrib.auth.models import User
8
from django.db import models
9
from django import forms
10

    
11
from calebasse.agenda import managers
12
from calebasse.utils import weeks_since_epoch, weekday_ranks
13
from calebasse.personnes.models import Holiday
14

    
15
from ..middleware.request import get_request
16

    
17
from interval import Interval
18

    
19
__all__ = (
20
    'EventType',
21
    'Event',
22
    'EventWithAct',
23
)
24

    
25
class EventType(models.Model):
26
    '''
27
    Simple ``Event`` classifcation.
28
    '''
29
    class Meta:
30
        verbose_name = u'Type d\'événement'
31
        verbose_name_plural = u'Types d\'événement'
32

    
33
    def __unicode__(self):
34
        return self.label
35

    
36
    label = models.CharField(_('label'), max_length=50)
37
    rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
38

    
39
class Event(models.Model):
40
    '''
41
    Container model for general agenda events
42
    '''
43
    objects = managers.EventManager()
44

    
45
    title = models.CharField(_('Title'), max_length=60, blank=True, default="")
46
    description = models.TextField(_('Description'), max_length=100, blank=True, null=True)
47
    event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
48
    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

    
51
    services = models.ManyToManyField('ressources.Service',
52
            null=True, blank=True, default=None)
53
    participants = models.ManyToManyField('personnes.People',
54
            null=True, blank=True, default=None)
55
    ressource = models.ForeignKey('ressources.Ressource', blank=True, null=True,
56
                                  verbose_name=u'Ressource')
57

    
58
    start_datetime = models.DateTimeField(_('Début'), db_index=True)
59
    end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
60
    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
    # only used when there is no rr id
63
    old_rs_id = models.CharField(max_length=8, blank=True, null=True)
64
    # 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
            verbose_name=u'Reporté du', db_index=True)
71
    # canceled can only be used with exception to
72
    canceled = models.BooleanField(_('Annulé'), db_index=True)
73

    
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
    OFFSET = range(0, 4)
82
    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
            (13, u'La cinquième semaine du mois'),
93
            (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
            (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
    )
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
            null=True,
120
            db_index=True)
121
    recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
122
    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
    recurrence_week_rank = models.IntegerField(
135
            verbose_name=u"Rang de la semaine dans le mois",
136
            choices=WEEK_RANKS,
137
            blank=True, null=True, db_index=True)
138
    recurrence_week_parity = models.PositiveIntegerField(
139
            choices=PARITIES,
140
            verbose_name=u"Parité des semaines",
141
            blank=True,
142
            null=True,
143
            db_index=True)
144
    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
    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
        (None, -1, None),
159
        (None, None, 0),
160
        (None, None, 1),
161
        (None, 4, None),
162
    ]
163

    
164
    class Meta:
165
        verbose_name = u'Evénement'
166
        verbose_name_plural = u'Evénements'
167
        ordering = ('start_datetime', 'end_datetime', 'title')
168
        unique_together = (('exception_to', 'exception_date'),)
169

    
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
        self.sanitize()
178
        if self.recurrence_periodicity:
179
            if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
180
                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
                    raise forms.ValidationError(u'Le date de départ de la périodicité est en semaine {week}.'.format(week=week))
187
        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

    
192
    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
    def timedelta(self):
207
        '''Distance between start and end of the event'''
208
        return self.end_datetime - self.start_datetime
209

    
210
    def match_date(self, date):
211
        if self.is_recurring():
212
            # consider exceptions
213
            exception = self.get_exceptions_dict().get(date)
214
            if exception is not None:
215
                return exception if exception.match_date(date) else None
216
            if self.canceled:
217
                return None
218
            if date.weekday() != self.recurrence_week_day:
219
                return None
220
            if self.start_datetime.date() > date:
221
                return None
222
            if self.recurrence_end_date and self.recurrence_end_date < date:
223
                return None
224
            if self.recurrence_week_period is not None:
225
                if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
226
                    return None
227
            elif self.recurrence_week_parity is not None:
228
                if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
229
                    return None
230
            elif self.recurrence_week_rank is not None:
231
                if self.recurrence_week_rank not in weekday_ranks(date):
232
                    return None
233
            else:
234
                raise NotImplemented
235
            return self
236
        else:
237
            return self if date == self.start_datetime.date() else None
238

    
239

    
240
    def today_occurrence(self, today=None, match=False, upgrade=True):
241
        '''For a recurring event compute the today 'Event'.
242

    
243
           The computed event is the fake one that you cannot save to the database.
244
        '''
245
        today = today or date.today()
246
        if self.canceled:
247
            return None
248
        if match:
249
            exception = self.get_exceptions_dict().get(today)
250
            if exception:
251
                if exception.start_datetime.date() == today:
252
                    return exception.today_occurrence(today)
253
                else:
254
                    return None
255
        else:
256
            exception_or_self = self.match_date(today)
257
            if exception_or_self is None:
258
                return None
259
            if exception_or_self != self:
260
                return exception_or_self.today_occurrence(today)
261
        if self.event_type_id == 1 and type(self) != EventWithAct and upgrade:
262
           self = self.eventwithact
263
        if self.recurrence_periodicity is None:
264
            return self
265
        start_datetime = datetime.combine(today, self.start_datetime.timetz())
266
        end_datetime = start_datetime + self.timedelta()
267
        event = copy(self)
268
        event.exception_to = self
269
        event.exception_date = today
270
        event.start_datetime = start_datetime
271
        event.end_datetime = end_datetime
272
        event.recurrence_periodicity = None
273
        event.recurrence_week_offset = 0
274
        event.recurrence_week_period = None
275
        event.recurrence_week_parity = None
276
        event.recurrence_week_rank = None
277
        event.recurrence_end_date = None
278
        event.parent = self
279
        # the returned event is "virtual", it must not be saved
280
        old_save = event.save
281
        old_participants = list(self.participants.all())
282
        old_services = list(self.services.all())
283
        def save(*args, **kwargs):
284
            event.id = None
285
            event.event_ptr_id = None
286
            old_save(*args, **kwargs)
287
            if hasattr(self, 'exceptions_dict'):
288
                self.exceptions_dict[event.start_datetime.date()] = event
289
            event.services = old_services
290
            event.participants = old_participants
291
            event.save = old_save
292
        event.save = save
293
        return event
294

    
295
    def next_occurence(self, today=None):
296
        '''Returns the next occurence after today.'''
297
        today = today or date.today()
298
        for occurence in self.all_occurences():
299
            if occurence.start_datetime.date() > today:
300
                return occurence
301

    
302
    def is_recurring(self):
303
        '''Is this event multiple ?'''
304
        return self.recurrence_periodicity is not None
305

    
306
    def get_exceptions_dict(self):
307
        if not hasattr(self, 'exceptions_dict'):
308
            self.exceptions_dict = dict()
309
            if self.exception_to_id is None:
310
                for exception in self.exceptions.all():
311
                    self.exceptions_dict[exception.exception_date] = exception
312
        return self.exceptions_dict
313

    
314
    def all_occurences(self, limit=90):
315
        '''Returns all occurences of this event as virtual Event objects
316

    
317
           limit - compute occurrences until limit days in the future
318

    
319
           Default is to limit to 90 days.
320
        '''
321
        if self.recurrence_periodicity is not None:
322
            day = self.start_datetime.date()
323
            max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
324
            end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
325
            occurrences = []
326
            if self.recurrence_week_period is not None:
327
                delta = timedelta(days=self.recurrence_week_period*7)
328
                while day <= end_date:
329
                    occurrence = self.today_occurrence(day, True)
330
                    if occurrence is not None:
331
                        occurrences.append(occurrence)
332
                    day += delta
333
            elif self.recurrence_week_parity is not None:
334
                delta = timedelta(days=7)
335
                while day <= end_date:
336
                    if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
337
                        occurrence = self.today_occurrence(day, True)
338
                        if occurrence is not None:
339
                            occurrences.append(occurrence)
340
                    day += delta
341
            elif self.recurrence_week_rank is not None:
342
                delta = timedelta(days=7)
343
                while day <= end_date:
344
                    if self.recurrence_week_rank in weekday_ranks(day):
345
                        occurrence = self.today_occurrence(day, True)
346
                        if occurrence is not None:
347
                            occurrences.append(occurrence)
348
                    day += delta
349
            for exception in self.exceptions.all():
350
                if not exception.canceled:
351
                    if exception.exception_date != exception.start_datetime.date() or exception.exception_date > end_date:
352
                        occurrences.append(exception.eventwithact if exception.event_type_id == 1 else exception)
353
            return sorted(occurrences, key=lambda o: o.start_datetime)
354
        else:
355
            return [self]
356

    
357
    def save(self, *args, **kwargs):
358
        assert self.recurrence_periodicity is None or self.exception_to is None
359
        assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
360
        assert self.start_datetime is not None
361
        self.sanitize() # init periodicity fields
362
        super(Event, self).save(*args, **kwargs)
363
        self.acts_cleaning()
364

    
365
    def delete(self, *args, **kwargs):
366
        self.canceled = True
367
        # save will clean acts
368
        self.save(*args, **kwargs)
369

    
370
    def acts_cleaning(self):
371
        # list of occurences may have changed
372
        from ..actes.models import Act
373
        if self.exception_to:
374
            # maybe a new exception, so look for parent acts with same date
375
            # as exception date
376
            acts = Act.objects.filter(models.Q(parent_event=self)
377
                    |models.Q(parent_event=self.exception_to,
378
                        date=self.exception_date))
379
        else:
380
            acts = Act.objects.filter(parent_event=self)
381
        acts = acts.prefetch_related('actvalidationstate_set')
382
        if acts:
383
            eventwithact = self.eventwithact
384
            for act in acts:
385
                if act.is_billed:
386
                    pass
387
                occurrence = eventwithact.today_occurrence(act.date)
388
                if occurrence:
389
                    occurrence.update_act(act)
390
                else:
391
                    act.delete()
392

    
393
    def to_interval(self):
394
        return Interval(self.start_datetime, self.end_datetime)
395

    
396
    def is_event_absence(self):
397
        return False
398

    
399
    def get_inactive_participants(self):
400
        return self.participants.filter(worker__enabled=False)
401

    
402
    def get_missing_participants(self):
403
        missing_participants = []
404
        for participant in self.participants.all():
405
            holidays = None
406
            worker = participant.worker
407
            holidays = Holiday.objects.for_worker(worker) \
408
                  .for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
409
            if holidays:
410
                missing_participants.append(participant)
411
        return missing_participants
412

    
413
    RECURRENCE_DESCRIPTION = [
414
            u'Tous les %s',      #(1, None, None),
415
            u'Un %s sur deux',   #(2, None, None),
416
            u'Un %s sur trois',  #(3, None, None),
417
            u'Un %s sur quatre', #(4, None, None),
418
            u'Un %s sur cinq',   #(5, None, None),
419
            u'Le premier %s du mois',   #(None, 0, None),
420
            u'Le deuxième %s du mois',  #(None, 1, None),
421
            u'Le troisième %s du mois', #(None, 2, None),
422
            u'Le quatrième %s du mois', #(None, 3, None),
423
            u'Le dernier %s du mois',   #(None, 4, None),
424
            u'Les %s les semaines paires',    #(None, None, 0),
425
            u'Les %s les semaines impaires', #(None, None, 1)
426
    ]
427

    
428
    WEEKDAY_DESRIPTION = [
429
            u'lundi',
430
            u'mardi',
431
            u'mercredi',
432
            u'jeudi',
433
            u'vendredi',
434
            u'samedi',
435
            u'dimanche'
436
    ]
437

    
438
    def recurrence_description(self):
439
        '''Self description of this recurring event'''
440
        if not self.recurrence_periodicity:
441
            return None
442
        parts = []
443
        parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
444
            % self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
445
        if self.recurrence_end_date:
446
            parts.append(u'du')
447
        else:
448
            parts.append(u'à partir du')
449
        parts.append(self.start_datetime.strftime('%d/%m/%Y'))
450
        if self.recurrence_end_date:
451
            parts.append(u'au')
452
            parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
453
        return u' '.join(parts)
454

    
455
    def is_absent(self):
456
        try:
457
            return self.eventwithact.is_absent()
458
        except self.DoesNotExist:
459
            return False
460

    
461
    def __unicode__(self):
462
        return self.title
463

    
464
    def __repr__(self):
465
        return '<Event: on {start_datetime} with {participants}'.format(
466
                start_datetime=self.start_datetime,
467
                participants=self.participants.all() if self.id else '<un-saved>')
468

    
469

    
470
class EventWithActManager(managers.EventManager):
471
    def create_patient_appointment(self, creator, title, patient,
472
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
473
            ressource=None, periodicity=1, until=False):
474
        appointment = self.create_event(creator=creator,
475
                title=title,
476
                event_type=EventType(id=1),
477
                participants=doctors,
478
                services=[service],
479
                start_datetime=start_datetime,
480
                end_datetime=end_datetime,
481
                ressource=ressource,
482
                periodicity=periodicity,
483
                until=until,
484
                act_type=act_type,
485
                patient=patient)
486
        return appointment
487

    
488

    
489
class EventWithAct(Event):
490
    '''An event corresponding to an act.'''
491
    objects = EventWithActManager()
492
    act_type = models.ForeignKey('ressources.ActType',
493
        verbose_name=u'Type d\'acte')
494
    patient = models.ForeignKey('dossiers.PatientRecord')
495
    convocation_sent = models.BooleanField(blank=True,
496
        verbose_name=u'Convoqué', db_index=True)
497

    
498

    
499
    @property
500
    def act(self):
501
        for act in self.act_set.all():
502
            if act.date == self.start_datetime.date():
503
                return act
504
        return self.build_act()
505

    
506
    def get_state(self):
507
        act = self.act
508
        if act.id:
509
            return act.get_state()
510
        return None
511

    
512
    def is_absent(self):
513
        act = self.act
514
        if act.id:
515
            return act.is_absent()
516
        return False
517

    
518
    def build_act(self):
519
        from ..actes.models import Act, ActValidationState
520
        act = Act()
521
        self.init_act(act)
522
        old_save = act.save
523
        def save(*args, **kwargs):
524
            act.save = old_save
525
            old_save(*args, **kwargs)
526
            act.comment = self.description
527
            act.doctors = (participant.worker for participant in self.participants.all())
528
            last_validation_state = ActValidationState.objects.create(
529
                    act=act, state_name='NON_VALIDE',
530
                    author=self.creator, previous_state=None)
531
            act.last_validation_state = last_validation_state
532
            old_save(*args, **kwargs)
533
        act.save = save
534
        return act
535

    
536
    def update_act(self, act):
537
        '''Update an act to match details of the meeting'''
538
        self.init_act(act)
539
        act.save()
540

    
541
    def init_act(self, act):
542
        delta = self.timedelta()
543
        duration = delta.seconds // 60
544
        act._duration = duration
545
        act.act_type = self.act_type
546
        act.patient = self.patient
547
        act.parent_event = self
548
        act.date = self.start_datetime.date()
549
        act.time = self.start_datetime.time()
550

    
551
    def save(self, *args, **kwargs):
552
        '''Force event_type to be patient meeting.'''
553
        self.event_type = EventType(id=1)
554
        super(EventWithAct, self).save(*args, **kwargs)
555

    
556
    def is_event_absence(self):
557
        return self.act.is_absent()
558

    
559
    def __unicode__(self):
560
        kwargs = {
561
                'patient': self.patient,
562
                'start_datetime': self.start_datetime,
563
                'act_type': self.act_type
564
        }
565
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
566
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
567
            '{act_type} ({act_type.id})'.format(**kwargs)
568

    
569

    
570
from django.db.models.signals import m2m_changed
571
from django.dispatch import receiver
572

    
573

    
574
@receiver(m2m_changed, sender=Event.participants.through)
575
def participants_changed(sender, instance, action, **kwargs):
576
    if action.startswith('post'):
577
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
578
        for act in instance.act_set.all():
579
            act.doctors = workers
(7-7/10)