Project

General

Profile

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

calebasse / calebasse / agenda / models.py @ bba85766

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
from interval import Interval
15

    
16
__all__ = (
17
    'EventType',
18
    'Event',
19
    'EventWithAct',
20
)
21

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

    
30
    def __unicode__(self):
31
        return self.label
32

    
33
    label = models.CharField(_('label'), max_length=50)
34
    rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
35

    
36
class Event(models.Model):
37
    '''
38
    Container model for general agenda events
39
    '''
40
    objects = managers.EventManager()
41

    
42
    title = models.CharField(_('Title'), max_length=60, blank=True, default="")
43
    description = models.TextField(_('Description'), max_length=100, blank=True, null=True)
44
    event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
45
    creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
46
    create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
47

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

    
55
    start_datetime = models.DateTimeField(_('Début'), db_index=True)
56
    end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
57
    old_ev_id = models.CharField(max_length=8, blank=True, null=True)
58
    old_rr_id = models.CharField(max_length=8, blank=True, null=True)
59
    # only used when there is no rr id
60
    old_rs_id = models.CharField(max_length=8, blank=True, null=True)
61
    # exception to is mutually exclusive with recurrence_periodicity
62
    # an exception cannot be periodic
63
    exception_to = models.ForeignKey('self', related_name='exceptions',
64
            blank=True, null=True,
65
            verbose_name=u'Exception à')
66
    exception_date = models.DateField(blank=True, null=True,
67
            verbose_name=u'Reporté du', db_index=True)
68
    # canceled can only be used with exception to
69
    canceled = models.BooleanField(_('Annulé'), db_index=True)
70

    
71
    PERIODS = (
72
            (1, u'Toutes les semaines'),
73
            (2, u'Une semaine sur deux'),
74
            (3, u'Une semaine sur trois'),
75
            (4, 'Une semaine sur quatre'),
76
            (5, 'Une semaine sur cinq')
77
    )
78
    OFFSET = range(0, 4)
79
    PERIODICITIES = (
80
            (1, u'Toutes les semaines'),
81
            (2, u'Une semaine sur deux'),
82
            (3, u'Une semaine sur trois'),
83
            (4, u'Une semaine sur quatre'),
84
            (5, u'Une semaine sur cinq'),
85
            (6, u'La première semaine du mois'),
86
            (7, u'La deuxième semaine du mois'),
87
            (8, u'La troisième semaine du mois'),
88
            (9, u'La quatrième semaine du mois'),
89
            (13, u'La cinquième semaine du mois'),
90
            (10, u'La dernière semaine du mois'),
91
            (11, u'Les semaines paires'),
92
            (12, u'Les semaines impaires')
93
    )
94
    WEEK_RANKS = (
95
            (0, u'La première semaine du mois'),
96
            (1, u'La deuxième semaine du mois'),
97
            (2, u'La troisième semaine du mois'),
98
            (3, u'La quatrième semaine du mois'),
99
            (4, u'La cinquième semaine du mois'),
100
            (-1, u'La dernière semaine du mois'),
101
#            (-2, u'L\'avant dernière semaine du mois'),
102
#            (-3, u'L\'antépénultième semaine du mois'),
103
#            (-4, u'L\'anté-antépénultième semaine du mois'),
104
#            (-5, u'L\'anté-anté-antépénultième semaine du mois')
105

    
106
    )
107
    PARITIES = (
108
            (0, u'Les semaines paires'),
109
            (1, u'Les semaines impaires')
110
    )
111
    recurrence_periodicity = models.PositiveIntegerField(
112
            choices=PERIODICITIES,
113
            verbose_name=u"Périodicité",
114
            default=None,
115
            blank=True,
116
            null=True,
117
            db_index=True)
118
    recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
119
    recurrence_week_offset = models.PositiveIntegerField(
120
            choices=zip(OFFSET, OFFSET),
121
            verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
122
            default=0,
123
            db_index=True)
124
    recurrence_week_period = models.PositiveIntegerField(
125
            choices=PERIODS,
126
            verbose_name=u"Période en semaines",
127
            default=None,
128
            blank=True,
129
            null=True,
130
            db_index=True)
131
    recurrence_week_rank = models.IntegerField(
132
            verbose_name=u"Rang de la semaine dans le mois",
133
            choices=WEEK_RANKS,
134
            blank=True, null=True, db_index=True)
135
    recurrence_week_parity = models.PositiveIntegerField(
136
            choices=PARITIES,
137
            verbose_name=u"Parité des semaines",
138
            blank=True,
139
            null=True,
140
            db_index=True)
141
    recurrence_end_date = models.DateField(
142
            verbose_name=_(u'Fin de la récurrence'),
143
            blank=True, null=True,
144
            db_index=True)
145

    
146
    PERIOD_LIST_TO_FIELDS = [(1, None, None),
147
        (2, None, None),
148
        (3, None, None),
149
        (4, None, None),
150
        (5, None, None),
151
        (None, 0, None),
152
        (None, 1, None),
153
        (None, 2, None),
154
        (None, 3, None),
155
        (None, -1, None),
156
        (None, None, 0),
157
        (None, None, 1),
158
        (None, 4, None),
159
    ]
160

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

    
167
    def __init__(self, *args, **kwargs):
168
        if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
169
            kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
170
        super(Event, self).__init__(*args, **kwargs)
171

    
172
    def clean(self):
173
        '''Initialize recurrence fields if they are not.'''
174
        self.sanitize()
175
        if self.recurrence_periodicity:
176
            if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
177
                raise forms.ValidationError(u'La date de fin de périodicité doit être postérieure à la date de début.')
178
        if self.recurrence_week_parity is not None:
179
            if self.start_datetime:
180
                week = self.start_datetime.date().isocalendar()[1]
181
                start_week_parity = week % 2
182
                if start_week_parity != self.recurrence_week_parity:
183
                    raise forms.ValidationError(u'Le date de départ de la périodicité est en semaine {week}.'.format(week=week))
184
        if self.recurrence_week_rank is not None and self.start_datetime:
185
            start_week_ranks = weekday_ranks(self.start_datetime.date())
186
            if self.recurrence_week_rank not in start_week_ranks:
187
                raise forms.ValidationError('La date de début de périodicité doit faire partie de la bonne semaine dans le mois.')
188

    
189
    def sanitize(self):
190
        if self.recurrence_periodicity:
191
            l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
192
        else:
193
            l = None, None, None
194
        self.recurrence_week_period = l[0]
195
        self.recurrence_week_rank = l[1]
196
        self.recurrence_week_parity = l[2]
197
        if self.start_datetime:
198
            if self.recurrence_periodicity:
199
                self.recurrence_week_day = self.start_datetime.weekday()
200
            if self.recurrence_week_period is not None:
201
                self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
202

    
203
    def timedelta(self):
204
        '''Distance between start and end of the event'''
205
        return self.end_datetime - self.start_datetime
206

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

    
236

    
237
    def today_occurrence(self, today=None, match=False, upgrade=True):
238
        '''For a recurring event compute the today 'Event'.
239

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

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

    
299
    def is_recurring(self):
300
        '''Is this event multiple ?'''
301
        return self.recurrence_periodicity is not None
302

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

    
311
    def all_occurences(self, limit=90):
312
        '''Returns all occurences of this event as virtual Event objects
313

    
314
           limit - compute occurrences until limit days in the future
315

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

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

    
362
    def delete(self, *args, **kwargs):
363
        self.canceled = True
364
        # save will clean acts
365
        self.save(*args, **kwargs)
366

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

    
390
    def to_interval(self):
391
        return Interval(self.start_datetime, self.end_datetime)
392

    
393
    def is_event_absence(self):
394
        return False
395

    
396
    def get_inactive_participants(self):
397
        return self.participants.filter(worker__enabled=False)
398

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

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

    
425
    WEEKDAY_DESRIPTION = [
426
            u'lundi',
427
            u'mardi',
428
            u'mercredi',
429
            u'jeudi',
430
            u'vendredi',
431
            u'samedi',
432
            u'dimanche'
433
    ]
434

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

    
452
    def __unicode__(self):
453
        return self.title
454

    
455
    def __repr__(self):
456
        return '<Event: on {start_datetime} with {participants}'.format(
457
                start_datetime=self.start_datetime,
458
                participants=self.participants.all() if self.id else '<un-saved>')
459

    
460

    
461
class EventWithActManager(managers.EventManager):
462
    def create_patient_appointment(self, creator, title, patient,
463
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
464
            ressource=None, periodicity=1, until=False):
465
        appointment = self.create_event(creator=creator,
466
                title=title,
467
                event_type=EventType(id=1),
468
                participants=doctors,
469
                services=[service],
470
                start_datetime=start_datetime,
471
                end_datetime=end_datetime,
472
                ressource=ressource,
473
                periodicity=periodicity,
474
                until=until,
475
                act_type=act_type,
476
                patient=patient)
477
        return appointment
478

    
479

    
480
class EventWithAct(Event):
481
    '''An event corresponding to an act.'''
482
    objects = EventWithActManager()
483
    act_type = models.ForeignKey('ressources.ActType',
484
        verbose_name=u'Type d\'acte')
485
    patient = models.ForeignKey('dossiers.PatientRecord')
486
    convocation_sent = models.BooleanField(blank=True,
487
        verbose_name=u'Convoqué', db_index=True)
488

    
489

    
490
    @property
491
    def act(self):
492
        for act in self.act_set.all():
493
            if act.date == self.start_datetime.date():
494
                return act
495
        return self.build_act()
496

    
497
    def get_state(self):
498
        act = self.act
499
        if act.id:
500
            return act.get_state()
501
        return None
502

    
503
    def is_absent(self):
504
        act = self.act
505
        if act.id:
506
            return act.is_absent()
507
        return False
508

    
509
    def build_act(self):
510
        from ..actes.models import Act, ActValidationState
511
        act = Act()
512
        self.init_act(act)
513
        old_save = act.save
514
        def save(*args, **kwargs):
515
            act.save = old_save
516
            old_save(*args, **kwargs)
517
            act.comment = self.description
518
            act.doctors = self.participants.select_subclasses()
519
            last_validation_state = ActValidationState.objects.create(
520
                    act=act, state_name='NON_VALIDE',
521
                    author=self.creator, previous_state=None)
522
            act.last_validation_state = last_validation_state
523
            old_save(*args, **kwargs)
524
        act.save = save
525
        return act
526

    
527
    def update_act(self, act):
528
        '''Update an act to match details of the meeting'''
529
        self.init_act(act)
530
        act.save()
531

    
532
    def init_act(self, act):
533
        delta = self.timedelta()
534
        duration = delta.seconds // 60
535
        act._duration = duration
536
        act.act_type = self.act_type
537
        act.patient = self.patient
538
        act.parent_event = self
539
        act.date = self.start_datetime.date()
540
        act.time = self.start_datetime.time()
541

    
542
    def save(self, *args, **kwargs):
543
        '''Force event_type to be patient meeting.'''
544
        self.event_type = EventType(id=1)
545
        super(EventWithAct, self).save(*args, **kwargs)
546

    
547
    def is_event_absence(self):
548
        return self.act.is_absent()
549

    
550
    def __unicode__(self):
551
        kwargs = {
552
                'patient': self.patient,
553
                'start_datetime': self.start_datetime,
554
                'act_type': self.act_type
555
        }
556
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
557
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
558
            '{act_type} ({act_type.id})'.format(**kwargs)
559

    
560

    
561
from django.db.models.signals import m2m_changed
562
from django.dispatch import receiver
563

    
564

    
565
@receiver(m2m_changed, sender=Event.participants.through)
566
def participants_changed(sender, instance, action, **kwargs):
567
    if action.startswith('post'):
568
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
569
        for act in instance.act_set.all():
570
            act.doctors = workers
(7-7/10)