Project

General

Profile

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

calebasse / calebasse / agenda / models.py @ 1f4ae3b3

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
    @property
211
    def time(self):
212
        return self.start_datetime.time()
213

    
214
    @property
215
    def date(self):
216
        return self.start_datetime.date()
217

    
218
    @property
219
    def duration(self):
220
        if self.timedelta():
221
            return self.timedelta().seconds / 60
222
        return 0
223

    
224
    def match_date(self, date):
225
        if self.is_recurring():
226
            # consider exceptions
227
            exception = self.get_exceptions_dict().get(date)
228
            if exception is not None:
229
                return exception if exception.match_date(date) else None
230
            if self.canceled:
231
                return None
232
            if date.weekday() != self.recurrence_week_day:
233
                return None
234
            if self.start_datetime.date() > date:
235
                return None
236
            if self.recurrence_end_date and self.recurrence_end_date < date:
237
                return None
238
            if self.recurrence_week_period is not None:
239
                if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
240
                    return None
241
            elif self.recurrence_week_parity is not None:
242
                if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
243
                    return None
244
            elif self.recurrence_week_rank is not None:
245
                if self.recurrence_week_rank not in weekday_ranks(date):
246
                    return None
247
            else:
248
                raise NotImplemented
249
            return self
250
        else:
251
            return self if date == self.start_datetime.date() else None
252

    
253

    
254
    def today_occurrence(self, today=None, match=False, upgrade=True):
255
        '''For a recurring event compute the today 'Event'.
256

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

    
309
    def next_occurence(self, today=None):
310
        '''Returns the next occurence after today.'''
311
        today = today or date.today()
312
        for occurence in self.all_occurences():
313
            if occurence.start_datetime.date() > today:
314
                return occurence
315

    
316
    def is_recurring(self):
317
        '''Is this event multiple ?'''
318
        return self.recurrence_periodicity is not None
319

    
320
    def get_exceptions_dict(self):
321
        if not hasattr(self, 'exceptions_dict'):
322
            self.exceptions_dict = dict()
323
            if self.exception_to_id is None:
324
                for exception in self.exceptions.all():
325
                    self.exceptions_dict[exception.exception_date] = exception
326
        return self.exceptions_dict
327

    
328
    def all_occurences(self, limit=90):
329
        '''Returns all occurences of this event as virtual Event objects
330

    
331
           limit - compute occurrences until limit days in the future
332

    
333
           Default is to limit to 90 days.
334
        '''
335
        if self.recurrence_periodicity is not None:
336
            day = self.start_datetime.date()
337
            max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
338
            end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
339
            occurrences = []
340
            if self.recurrence_week_period is not None:
341
                delta = timedelta(days=self.recurrence_week_period*7)
342
                while day <= end_date:
343
                    occurrence = self.today_occurrence(day, True)
344
                    if occurrence is not None:
345
                        occurrences.append(occurrence)
346
                    day += delta
347
            elif self.recurrence_week_parity is not None:
348
                delta = timedelta(days=7)
349
                while day <= end_date:
350
                    if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
351
                        occurrence = self.today_occurrence(day, True)
352
                        if occurrence is not None:
353
                            occurrences.append(occurrence)
354
                    day += delta
355
            elif self.recurrence_week_rank is not None:
356
                delta = timedelta(days=7)
357
                while day <= end_date:
358
                    if self.recurrence_week_rank in weekday_ranks(day):
359
                        occurrence = self.today_occurrence(day, True)
360
                        if occurrence is not None:
361
                            occurrences.append(occurrence)
362
                    day += delta
363
            for exception in self.exceptions.all():
364
                if not exception.canceled:
365
                    if exception.exception_date != exception.start_datetime.date() or exception.exception_date > end_date:
366
                        occurrences.append(exception.eventwithact if exception.event_type_id == 1 else exception)
367
            return sorted(occurrences, key=lambda o: o.start_datetime)
368
        else:
369
            return [self]
370

    
371
    def save(self, *args, **kwargs):
372
        assert self.recurrence_periodicity is None or self.exception_to is None
373
        assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
374
        assert self.start_datetime is not None
375
        self.sanitize() # init periodicity fields
376
        super(Event, self).save(*args, **kwargs)
377
        self.events_cleaning()
378
        self.acts_cleaning()
379

    
380
    def delete(self, *args, **kwargs):
381
        if not self.one_act_already_billed():
382
            for exception in self.exceptions.all():
383
                exception.delete()
384
            self.canceled = True
385
            if hasattr(self, 'eventwithact'):
386
                # Needed
387
                self.eventwithact.canceled = True
388
            # save will clean acts
389
            self.save(*args, **kwargs)
390

    
391
    @property
392
    def acts(self):
393
        from ..actes.models import Act
394
        return Act.objects.filter(
395
                models.Q(parent_event=self) | \
396
                models.Q(parent_event__exception_to=self))
397

    
398
    def one_act_already_billed(self):
399
        '''
400
            Return True if at least one act of the present event or an act
401
            of one of its exceptions is already billed.
402
        '''
403
        return self.acts.filter(already_billed=True).exists()
404

    
405
    def one_act_is_billed(self):
406
        '''
407
            Return True if at least one act of the present event or an act
408
            of one of its exceptions is billed.
409
        '''
410
        return self.acts.filter(is_billed=True).exists()
411

    
412
    def events_cleaning(self):
413
        """
414
            Delete exceptions out of bounds if not with an associated act
415
            already billed.
416
        """
417
        events = None
418
        if self.recurrence_end_date:
419
            events = Event.objects.filter(
420
                models.Q(exception_to=self,
421
                    exception_date__lt=self.start_datetime.date()) | \
422
                models.Q(exception_to=self,
423
                    exception_date__gt=self.recurrence_end_date))
424
        else:
425
            events = Event.objects.filter(exception_to=self,
426
                    exception_date__lt=self.start_datetime.date())
427
        for event in events:
428
            if hasattr(event, 'eventwithact'):
429
                if not event.eventwithact.act.already_billed:
430
                    event.delete()
431
            else:
432
                event.delete()
433

    
434
    def acts_cleaning(self):
435
        # list of occurences may have changed
436
        from ..actes.models import Act
437
        if self.exception_to:
438
            # maybe a new exception, so look for parent acts with same date
439
            # as exception date
440
            acts = Act.objects.filter(models.Q(parent_event=self)
441
                    |models.Q(parent_event=self.exception_to,
442
                        date=self.exception_date))
443
        else:
444
            acts = Act.objects.filter(parent_event=self)
445
        acts = acts.prefetch_related('actvalidationstate_set')
446
        if acts:
447
            eventwithact = self.eventwithact
448
            for act in acts:
449
                occurrence = eventwithact.today_occurrence(act.date)
450
                if occurrence:
451
                    occurrence.update_act(act)
452
                else:
453
                    if not act.already_billed:
454
                        act.delete()
455

    
456
    def to_interval(self):
457
        return Interval(self.start_datetime, self.end_datetime)
458

    
459
    def is_event_absence(self):
460
        return False
461

    
462
    def get_inactive_participants(self):
463
        return self.participants.filter(worker__enabled=False)
464

    
465
    def get_missing_participants(self):
466
        missing_participants = []
467
        for participant in self.participants.all():
468
            holidays = None
469
            worker = participant.worker
470
            holidays = Holiday.objects.for_worker(worker) \
471
                  .for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
472
            if holidays:
473
                missing_participants.append(participant)
474
        return missing_participants
475

    
476
    RECURRENCE_DESCRIPTION = [
477
            u'Tous les %s',      #(1, None, None),
478
            u'Un %s sur deux',   #(2, None, None),
479
            u'Un %s sur trois',  #(3, None, None),
480
            u'Un %s sur quatre', #(4, None, None),
481
            u'Un %s sur cinq',   #(5, None, None),
482
            u'Le premier %s du mois',   #(None, 0, None),
483
            u'Le deuxième %s du mois',  #(None, 1, None),
484
            u'Le troisième %s du mois', #(None, 2, None),
485
            u'Le quatrième %s du mois', #(None, 3, None),
486
            u'Le dernier %s du mois',   #(None, 4, None),
487
            u'Les %s les semaines paires',    #(None, None, 0),
488
            u'Les %s les semaines impaires', #(None, None, 1)
489
    ]
490

    
491
    WEEKDAY_DESRIPTION = [
492
            u'lundi',
493
            u'mardi',
494
            u'mercredi',
495
            u'jeudi',
496
            u'vendredi',
497
            u'samedi',
498
            u'dimanche'
499
    ]
500

    
501
    def recurrence_description(self):
502
        '''Self description of this recurring event'''
503
        if not self.recurrence_periodicity:
504
            return None
505
        parts = []
506
        parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
507
            % self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
508
        if self.recurrence_end_date:
509
            parts.append(u'du')
510
        else:
511
            parts.append(u'à partir du')
512
        parts.append(self.start_datetime.strftime('%d/%m/%Y'))
513
        if self.recurrence_end_date:
514
            parts.append(u'au')
515
            parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
516
        return u' '.join(parts)
517

    
518
    def is_absent(self):
519
        try:
520
            return self.eventwithact.is_absent()
521
        except self.DoesNotExist:
522
            return False
523

    
524
    def __unicode__(self):
525
        return self.title
526

    
527
    def __repr__(self):
528
        return '<Event: on {start_datetime} with {participants}'.format(
529
                start_datetime=self.start_datetime,
530
                participants=self.participants.all() if self.id else '<un-saved>')
531

    
532

    
533
class EventWithActManager(managers.EventManager):
534
    def create_patient_appointment(self, creator, title, patient,
535
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
536
            ressource=None, periodicity=1, until=False):
537
        appointment = self.create_event(creator=creator,
538
                title=title,
539
                event_type=EventType(id=1),
540
                participants=doctors,
541
                services=[service],
542
                start_datetime=start_datetime,
543
                end_datetime=end_datetime,
544
                ressource=ressource,
545
                periodicity=periodicity,
546
                until=until,
547
                act_type=act_type,
548
                patient=patient)
549
        return appointment
550

    
551

    
552
class EventWithAct(Event):
553
    '''An event corresponding to an act.'''
554
    objects = EventWithActManager()
555
    act_type = models.ForeignKey('ressources.ActType',
556
        verbose_name=u'Type d\'acte')
557
    patient = models.ForeignKey('dossiers.PatientRecord')
558
    convocation_sent = models.BooleanField(blank=True,
559
        verbose_name=u'Convoqué', db_index=True)
560

    
561

    
562
    @property
563
    def act(self):
564
        for act in self.act_set.all():
565
            if act.date == self.start_datetime.date():
566
                return act
567
        return self.build_act()
568

    
569
    def get_state(self):
570
        act = self.act
571
        if act.id:
572
            return act.get_state()
573
        return None
574

    
575
    def is_absent(self):
576
        act = self.act
577
        if act.id:
578
            return act.is_absent()
579
        return False
580

    
581
    def build_act(self):
582
        from ..actes.models import Act, ActValidationState
583
        act = Act()
584
        self.init_act(act)
585
        old_save = act.save
586
        def save(*args, **kwargs):
587
            act.save = old_save
588
            old_save(*args, **kwargs)
589
            act.doctors = (participant.worker for participant in self.participants.all())
590
            last_validation_state = ActValidationState.objects.create(
591
                    act=act, state_name='NON_VALIDE',
592
                    author=self.creator, previous_state=None)
593
            act.last_validation_state = last_validation_state
594
            old_save(*args, **kwargs)
595
        act.save = save
596
        return act
597

    
598
    def update_act(self, act):
599
        '''Update an act to match details of the meeting'''
600
        self.init_act(act)
601
        act.save()
602

    
603
    def init_act(self, act):
604
        if not act.is_billed:
605
            delta = self.timedelta()
606
            duration = delta.seconds // 60
607
            act._duration = duration
608
            act.act_type = self.act_type
609
            act.patient = self.patient
610
            act.date = self.start_datetime.date()
611
            act.time = self.start_datetime.time()
612
        act.parent_event = self
613

    
614
    def save(self, *args, **kwargs):
615
        '''Force event_type to be patient meeting.'''
616
        self.event_type = EventType(id=1)
617
        super(EventWithAct, self).save(*args, **kwargs)
618

    
619
    def is_event_absence(self):
620
        return self.act.is_absent()
621

    
622
    def __unicode__(self):
623
        kwargs = {
624
                'patient': self.patient,
625
                'start_datetime': self.start_datetime,
626
                'act_type': self.act_type
627
        }
628
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
629
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
630
            '{act_type} ({act_type.id})'.format(**kwargs)
631

    
632

    
633
from django.db.models.signals import m2m_changed
634
from django.dispatch import receiver
635

    
636

    
637
@receiver(m2m_changed, sender=Event.participants.through)
638
def participants_changed(sender, instance, action, **kwargs):
639
    if action.startswith('post'):
640
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
641
        for act in instance.act_set.all():
642
            act.doctors = workers
(7-7/10)