Project

General

Profile

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

calebasse / calebasse / agenda / models.py @ 0cbf950f

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)
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
    room = models.ForeignKey('ressources.Room', blank=True, null=True,
53
            verbose_name=u'Salle')
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
            (10, u'La dernière semaine du mois'),
90
            (11, u'Les semaines paires'),
91
            (12, u'Les semaines impaires')
92
    )
93
    WEEK_RANKS = (
94
            (0, u'La première semaine du mois'),
95
            (1, u'La deuxième semaine du mois'),
96
            (2, u'La troisième semaine du mois'),
97
            (3, u'La quatrième semaine du mois'),
98
            (4, u'La dernière semaine du mois')
99
    )
100
    PARITIES = (
101
            (0, u'Les semaines paires'),
102
            (1, u'Les semaines impaires')
103
    )
104
    recurrence_periodicity = models.PositiveIntegerField(
105
            choices=PERIODICITIES,
106
            verbose_name=u"Périodicité",
107
            default=None,
108
            blank=True,
109
            null=True,
110
            db_index=True)
111
    recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
112
    recurrence_week_offset = models.PositiveIntegerField(
113
            choices=zip(OFFSET, OFFSET),
114
            verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
115
            default=0,
116
            db_index=True)
117
    recurrence_week_period = models.PositiveIntegerField(
118
            choices=PERIODS,
119
            verbose_name=u"Période en semaines",
120
            default=None,
121
            blank=True,
122
            null=True,
123
            db_index=True)
124
    recurrence_week_rank = models.PositiveIntegerField(
125
            verbose_name=u"Rang de la semaine dans le mois",
126
            choices=WEEK_RANKS,
127
            blank=True, null=True, db_index=True)
128
    recurrence_week_parity = models.PositiveIntegerField(
129
            choices=PARITIES,
130
            verbose_name=u"Parité des semaines",
131
            blank=True,
132
            null=True,
133
            db_index=True)
134
    recurrence_end_date = models.DateField(
135
            verbose_name=_(u'Fin de la récurrence'),
136
            blank=True, null=True,
137
            db_index=True)
138

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

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

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

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

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

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

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

    
228

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

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

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

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

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

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

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

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

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

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

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

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

    
385
    def is_event_absence(self):
386
        return False
387

    
388
    def get_missing_participants(self):
389
        missing_participants = []
390
        for participant in self.participants.all():
391
            holidays = None
392
            worker = participant.worker
393
            holidays = Holiday.objects.for_worker(worker) \
394
                  .for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
395
            if holidays:
396
                missing_participants.append(participant)
397
        return missing_participants
398

    
399
    RECURRENCE_DESCRIPTION = [
400
            u'Tous les %s',      #(1, None, None),
401
            u'Un %s sur deux',   #(2, None, None),
402
            u'Un %s sur trois',  #(3, None, None),
403
            u'Un %s sur quatre', #(4, None, None),
404
            u'Un %s sur cinq',   #(5, None, None),
405
            u'Le premier %s du mois',   #(None, 0, None),
406
            u'Le deuxième %s du mois',  #(None, 1, None),
407
            u'Le troisième %s du mois', #(None, 2, None),
408
            u'Le quatrième %s du mois', #(None, 3, None),
409
            u'Le dernier %s du mois',   #(None, 4, None),
410
            u'Les %s les semaines paires',    #(None, None, 0),
411
            u'Les %s les semaines impaires', #(None, None, 1)
412
    ]
413

    
414
    WEEKDAY_DESRIPTION = [
415
            u'lundi',
416
            u'mardi',
417
            u'mercredi',
418
            u'jeudi',
419
            u'vendredi',
420
            u'samedi',
421
            u'dimanche'
422
    ]
423

    
424
    def recurrence_description(self):
425
        '''Self description of this recurring event'''
426
        if not self.recurrence_periodicity:
427
            return None
428
        parts = []
429
        parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
430
            % self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
431
        if self.recurrence_end_date:
432
            parts.append(u'du')
433
        else:
434
            parts.append(u'à partir du')
435
        parts.append(self.start_datetime.strftime('%d/%m/%Y'))
436
        if self.recurrence_end_date:
437
            parts.append(u'au')
438
            parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
439
        return u' '.join(parts)
440

    
441
    def __unicode__(self):
442
        return self.title
443

    
444
    def __repr__(self):
445
        return '<Event: on {start_datetime} with {participants}'.format(
446
                start_datetime=self.start_datetime,
447
                participants=self.participants.all() if self.id else '<un-saved>')
448

    
449

    
450
class EventWithActManager(managers.EventManager):
451
    def create_patient_appointment(self, creator, title, patient,
452
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
453
            room=None, periodicity=1, until=False):
454
        appointment = self.create_event(creator=creator,
455
                title=title,
456
                event_type=EventType(id=1),
457
                participants=doctors,
458
                services=[service],
459
                start_datetime=start_datetime,
460
                end_datetime=end_datetime,
461
                room=room,
462
                periodicity=periodicity,
463
                until=until,
464
                act_type=act_type,
465
                patient=patient)
466
        return appointment
467

    
468

    
469
class EventWithAct(Event):
470
    '''An event corresponding to an act.'''
471
    objects = EventWithActManager()
472
    act_type = models.ForeignKey('ressources.ActType',
473
        verbose_name=u'Type d\'acte')
474
    patient = models.ForeignKey('dossiers.PatientRecord')
475
    convocation_sent = models.BooleanField(blank=True,
476
        verbose_name=u'Convoqué', db_index=True)
477

    
478

    
479
    @property
480
    def act(self):
481
        for act in self.act_set.all():
482
            if act.date == self.start_datetime.date():
483
                return act
484
        return self.build_act()
485

    
486
    def get_state(self):
487
        act = self.act
488
        if act.id:
489
            return act.get_state()
490
        return None
491

    
492
    def is_absent(self):
493
        act = self.act
494
        if act.id:
495
            return act.is_absent()
496
        return False
497

    
498
    def build_act(self):
499
        from ..actes.models import Act, ActValidationState
500
        act = Act()
501
        self.init_act(act)
502
        old_save = act.save
503
        def save(*args, **kwargs):
504
            act.save = old_save
505
            old_save(*args, **kwargs)
506
            act.comment = self.description
507
            act.doctors = self.participants.select_subclasses()
508
            last_validation_state = ActValidationState.objects.create(
509
                    act=act, state_name='NON_VALIDE',
510
                    author=self.creator, previous_state=None)
511
            act.last_validation_state = last_validation_state
512
            old_save(*args, **kwargs)
513
        act.save = save
514
        return act
515

    
516
    def update_act(self, act):
517
        '''Update an act to match details of the meeting'''
518
        self.init_act(act)
519
        act.save()
520

    
521
    def init_act(self, act):
522
        delta = self.timedelta()
523
        duration = delta.seconds // 60
524
        act._duration = duration
525
        act.act_type = self.act_type
526
        act.patient = self.patient
527
        act.parent_event = self
528
        act.date = self.start_datetime.date()
529
        act.time = self.start_datetime.time()
530

    
531
    def save(self, *args, **kwargs):
532
        '''Force event_type to be patient meeting.'''
533
        self.event_type = EventType(id=1)
534
        super(EventWithAct, self).save(*args, **kwargs)
535

    
536
    def is_event_absence(self):
537
        return self.act.is_absent()
538

    
539
    def __unicode__(self):
540
        kwargs = {
541
                'patient': self.patient,
542
                'start_datetime': self.start_datetime,
543
                'act_type': self.act_type
544
        }
545
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
546
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
547
            '{act_type} ({act_type.id})'.format(**kwargs)
548

    
549

    
550
from django.db.models.signals import m2m_changed
551
from django.dispatch import receiver
552

    
553

    
554
@receiver(m2m_changed, sender=Event.participants.through)
555
def participants_changed(sender, instance, action, **kwargs):
556
    if action.startswith('post'):
557
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
558
        for act in instance.act_set.all():
559
            act.doctors = workers
(7-7/10)