Project

General

Profile

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

calebasse / calebasse / agenda / models.py @ 32551648

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 interval import Interval
14

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

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

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

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

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

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

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

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

    
70
    PERIODS = (
71
            (1, u'Toutes les semaines'),
72
            (2, u'Une semaine sur deux'),
73
            (3, u'Une semaine sur trois'),
74
            (4, 'Une semaine sur quatre'),
75
            (5, 'Une semaine sur cinq')
76
    )
77
    OFFSET = range(0,4)
78
    PERIODICITIES = (
79
            (1, u'Toutes les semaines'),
80
            (2, u'Une semaine sur deux'),
81
            (3, u'Une semaine sur trois'),
82
            (4, u'Une semaine sur quatre'),
83
            (5, u'Une semaine sur cinq'),
84
            (6, u'La première semaine du mois'),
85
            (7, u'La deuxième semaine du mois'),
86
            (8, u'La troisième semaine du mois'),
87
            (9, u'La quatrième semaine du mois'),
88
            (10, u'La dernière semaine du mois'),
89
            (11, u'Les semaines paires'),
90
            (12, u'Les semaines impaires')
91
    )
92
    WEEK_RANKS = (
93
            (0, u'La première semaine du mois'),
94
            (1, u'La deuxième semaine du mois'),
95
            (2, u'La troisième semaine du mois'),
96
            (3, u'La quatrième semaine du mois'),
97
            (4, u'La dernière semaine du mois')
98
    )
99
    PARITIES = (
100
            (0, u'Les semaines paires'),
101
            (1, u'Les semaines impaires')
102
    )
103
    recurrence_periodicity = models.PositiveIntegerField(
104
            choices=PERIODICITIES,
105
            verbose_name=u"Périodicité",
106
            default=None,
107
            blank=True,
108
            null=True,
109
            db_index=True)
110
    recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
111
    recurrence_week_offset = models.PositiveIntegerField(
112
            choices=zip(OFFSET, OFFSET),
113
            verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
114
            default=0,
115
            db_index=True)
116
    recurrence_week_period = models.PositiveIntegerField(
117
            choices=PERIODS,
118
            verbose_name=u"Période en semaines",
119
            default=None,
120
            blank=True,
121
            null=True,
122
            db_index=True)
123
    recurrence_week_rank = models.PositiveIntegerField(
124
            verbose_name=u"Rang de la semaine dans le mois",
125
            choices=WEEK_RANKS,
126
            blank=True, null=True, db_index=True)
127
    recurrence_week_parity = models.PositiveIntegerField(
128
            choices=PARITIES,
129
            verbose_name=u"Parité des semaines",
130
            blank=True,
131
            null=True,
132
            db_index=True)
133
    recurrence_end_date = models.DateField(
134
            verbose_name=_(u'Fin de la récurrence'),
135
            blank=True, null=True,
136
            db_index=True)
137

    
138
    PERIOD_LIST_TO_FIELDS = [(1, None, None),
139
        (2, None, None),
140
        (3, None, None),
141
        (4, None, None),
142
        (5, None, None),
143
        (None, 0, None),
144
        (None, 1, None),
145
        (None, 2, None),
146
        (None, 3, None),
147
        (None, 4, None),
148
        (None, None, 0),
149
        (None, None, 1)
150
    ]
151

    
152
    class Meta:
153
        verbose_name = u'Evénement'
154
        verbose_name_plural = u'Evénements'
155
        ordering = ('start_datetime', 'end_datetime', 'title')
156
        unique_together = (('exception_to', 'exception_date'),)
157

    
158
    def __init__(self, *args, **kwargs):
159
        if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
160
            kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
161
        super(Event, self).__init__(*args, **kwargs)
162

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

    
180
    def sanitize(self):
181
        if self.recurrence_periodicity:
182
            l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
183
        else:
184
            l = None, None, None
185
        self.recurrence_week_period = l[0]
186
        self.recurrence_week_rank = l[1]
187
        self.recurrence_week_parity = l[2]
188
        if self.start_datetime:
189
            if self.recurrence_periodicity:
190
                self.recurrence_week_day = self.start_datetime.weekday()
191
            if self.recurrence_week_period is not None:
192
                self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
193

    
194
    def timedelta(self):
195
        '''Distance between start and end of the event'''
196
        return self.end_datetime - self.start_datetime
197

    
198
    def match_date(self, date):
199
        if self.is_recurring():
200
            # consider exceptions
201
            exception = self.get_exceptions_dict().get(date)
202
            if exception is not None:
203
                return exception if exception.match_date(date) else None
204
            if self.canceled:
205
                return None
206
            if date.weekday() != self.recurrence_week_day:
207
                return None
208
            if self.start_datetime.date() > date:
209
                return None
210
            if self.recurrence_end_date and self.recurrence_end_date < date:
211
                return None
212
            if self.recurrence_week_period is not None:
213
                if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
214
                    return None
215
            elif self.recurrence_week_parity is not None:
216
                if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
217
                    return None
218
            elif self.recurrence_week_rank is not None:
219
                if self.recurrence_week_rank not in weekday_ranks(date):
220
                    return None
221
            else:
222
                raise NotImplemented
223
            return self
224
        else:
225
            return self if date == self.start_datetime.date() else None
226

    
227

    
228
    def today_occurrence(self, today=None, match=False, upgrade=True):
229
        '''For a recurring event compute the today 'Event'.
230

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

    
283
    def next_occurence(self, today=None):
284
        '''Returns the next occurence after today.'''
285
        today = today or date.today()
286
        for occurence in self.all_occurences():
287
            if occurence.start_datetime.date() > today:
288
                return occurence
289

    
290
    def is_recurring(self):
291
        '''Is this event multiple ?'''
292
        return self.recurrence_periodicity is not None
293

    
294
    def get_exceptions_dict(self):
295
        if not hasattr(self, 'exceptions_dict'):
296
            self.exceptions_dict = dict()
297
            if self.exception_to_id is None:
298
                for exception in self.exceptions.all():
299
                    self.exceptions_dict[exception.exception_date] = exception
300
        return self.exceptions_dict
301

    
302
    def all_occurences(self, limit=90):
303
        '''Returns all occurences of this event as virtual Event objects
304

    
305
           limit - compute occurrences until limit days in the future
306

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

    
345
    def save(self, *args, **kwargs):
346
        assert self.recurrence_periodicity is None or self.exception_to is None
347
        assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
348
        assert self.start_datetime is not None
349
        self.sanitize() # init periodicity fields
350
        super(Event, self).save(*args, **kwargs)
351
        self.acts_cleaning()
352

    
353
    def delete(self, *args, **kwargs):
354
        self.canceled = True
355
        # save will clean acts
356
        self.save(*args, **kwargs)
357

    
358
    def acts_cleaning(self):
359
        # list of occurences may have changed
360
        from ..actes.models import Act
361
        if self.exception_to:
362
            # maybe a new exception, so look for parent acts with same date
363
            # as exception date
364
            acts = Act.objects.filter(models.Q(parent_event=self)
365
                    |models.Q(parent_event=self.exception_to,
366
                        date=self.exception_date))
367
        else:
368
            acts = Act.objects.filter(parent_event=self)
369
        acts = acts.prefetch_related('actvalidationstate_set')
370
        if acts:
371
            eventwithact = self.eventwithact
372
            for act in acts:
373
                if act.is_billed:
374
                    pass
375
                occurrence = eventwithact.today_occurrence(act.date)
376
                if occurrence:
377
                    occurrence.update_act(act)
378
                else:
379
                    act.delete()
380

    
381
    def to_interval(self):
382
        return Interval(self.start_datetime, self.end_datetime)
383

    
384
    def is_event_absence(self):
385
        return False
386

    
387
    RECURRENCE_DESCRIPTION = [
388
            u'Tous les %s',      #(1, None, None),
389
            u'Un %s sur deux',   #(2, None, None),
390
            u'Un %s sur trois',  #(3, None, None),
391
            u'Un %s sur quatre', #(4, None, None),
392
            u'Un %s sur cinq',   #(5, None, None),
393
            u'Le premier %s du mois',   #(None, 0, None),
394
            u'Le deuxième %s du mois',  #(None, 1, None),
395
            u'Le troisième %s du mois', #(None, 2, None),
396
            u'Le quatrième %s du mois', #(None, 3, None),
397
            u'Le dernier %s du mois',   #(None, 4, None),
398
            u'Les %s les semaines paires',    #(None, None, 0),
399
            u'Les %s les semaines impaires', #(None, None, 1)
400
    ]
401

    
402
    WEEKDAY_DESRIPTION = [
403
            u'lundi',
404
            u'mardi',
405
            u'mercredi',
406
            u'jeudi',
407
            u'vendredi',
408
            u'samedi',
409
            u'dimanche'
410
    ]
411

    
412
    def recurrence_description(self):
413
        '''Self description of this recurring event'''
414
        if not self.recurrence_periodicity:
415
            return None
416
        parts = []
417
        parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
418
            % self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
419
        if self.recurrence_end_date:
420
            parts.append(u'du')
421
        else:
422
            parts.append(u'à partir du')
423
        parts.append(self.start_datetime.strftime('%d/%m/%Y'))
424
        if self.recurrence_end_date:
425
            parts.append(u'au')
426
            parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
427
        return u' '.join(parts)
428

    
429

    
430

    
431

    
432

    
433
    def __unicode__(self):
434
        return self.title
435

    
436
    def __repr__(self):
437
        return '<Event: on {start_datetime} with {participants}'.format(
438
                start_datetime=self.start_datetime,
439
                participants=self.participants.all() if self.id else '<un-saved>')
440

    
441

    
442
class EventWithActManager(managers.EventManager):
443
    def create_patient_appointment(self, creator, title, patient,
444
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
445
            room=None, periodicity=1, until=False):
446
        appointment = self.create_event(creator=creator,
447
                title=title,
448
                event_type=EventType(id=1),
449
                participants=doctors,
450
                services=[service],
451
                start_datetime=start_datetime,
452
                end_datetime=end_datetime,
453
                room=room,
454
                periodicity=periodicity,
455
                until=until,
456
                act_type=act_type,
457
                patient=patient)
458
        return appointment
459

    
460

    
461
class EventWithAct(Event):
462
    '''An event corresponding to an act.'''
463
    objects = EventWithActManager()
464
    act_type = models.ForeignKey('ressources.ActType',
465
        verbose_name=u'Type d\'acte')
466
    patient = models.ForeignKey('dossiers.PatientRecord')
467
    convocation_sent = models.BooleanField(blank=True,
468
        verbose_name=u'Convoqué', db_index=True)
469

    
470

    
471
    @property
472
    def act(self):
473
        for act in self.act_set.all():
474
            if act.date == self.start_datetime.date():
475
                return act
476
        return self.build_act()
477

    
478
    def get_state(self):
479
        act = self.act
480
        if act.id:
481
            return act.get_state()
482
        return None
483

    
484
    def is_absent(self):
485
        act = self.act
486
        if act.id:
487
            return act.is_absent()
488
        return False
489

    
490
    def build_act(self):
491
        from ..actes.models import Act, ActValidationState
492
        act = Act()
493
        self.init_act(act)
494
        old_save = act.save
495
        def save(*args, **kwargs):
496
            act.save = old_save
497
            old_save(*args, **kwargs)
498
            act.comment = self.description
499
            act.doctors = self.participants.select_subclasses()
500
            last_validation_state = ActValidationState.objects.create(
501
                    act=act, state_name='NON_VALIDE',
502
                    author=self.creator, previous_state=None)
503
            act.last_validation_state = last_validation_state
504
            old_save(*args, **kwargs)
505
        act.save = save
506
        return act
507

    
508
    def update_act(self, act):
509
        '''Update an act to match details of the meeting'''
510
        self.init_act(act)
511
        act.save()
512

    
513
    def init_act(self, act):
514
        delta = self.timedelta()
515
        duration = delta.seconds // 60
516
        act._duration = duration
517
        act.act_type = self.act_type
518
        act.patient = self.patient
519
        act.parent_event = self
520
        act.date = self.start_datetime.date()
521
        act.time = self.start_datetime.time()
522

    
523
    def save(self, *args, **kwargs):
524
        '''Force event_type to be patient meeting.'''
525
        self.event_type = EventType(id=1)
526
        super(EventWithAct, self).save(*args, **kwargs)
527

    
528
    def is_event_absence(self):
529
        return self.act.is_absent()
530

    
531
    def __unicode__(self):
532
        kwargs = {
533
                'patient': self.patient,
534
                'start_datetime': self.start_datetime,
535
                'act_type': self.act_type
536
        }
537
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
538
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
539
            '{act_type} ({act_type.id})'.format(**kwargs)
540

    
541

    
542
from django.db.models.signals import m2m_changed
543
from django.dispatch import receiver
544

    
545

    
546
@receiver(m2m_changed, sender=Event.participants.through)
547
def participants_changed(sender, instance, action, **kwargs):
548
    if action.startswith('post'):
549
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
550
        for act in instance.act_set.all():
551
            act.doctors = workers
(7-7/10)