Projet

Général

Profil

Télécharger (22,4 ko) Statistiques
| Branche: | Tag: | Révision:

calebasse / calebasse / agenda / models.py @ 7a2fc3bb

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 is_absent(self):
453
        try:
454
            return self.eventwithact.is_absent()
455
        except self.DoesNotExist:
456
            return False
457

    
458
    def __unicode__(self):
459
        return self.title
460

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

    
466

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

    
485

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

    
495

    
496
    @property
497
    def act(self):
498
        for act in self.act_set.all():
499
            if act.date == self.start_datetime.date():
500
                return act
501
        return self.build_act()
502

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

    
509
    def is_absent(self):
510
        act = self.act
511
        if act.id:
512
            return act.is_absent()
513
        return False
514

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

    
533
    def update_act(self, act):
534
        '''Update an act to match details of the meeting'''
535
        self.init_act(act)
536
        act.save()
537

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

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

    
553
    def is_event_absence(self):
554
        return self.act.is_absent()
555

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

    
566

    
567
from django.db.models.signals import m2m_changed
568
from django.dispatch import receiver
569

    
570

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