Projet

Général

Profil

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

calebasse / calebasse / agenda / models.py @ dd986559

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
            (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 cinquième semaine du mois'),
99
            (-1, u'La dernière semaine du mois'),
100
#            (-2, u'L\'avant dernière semaine du mois'),
101
#            (-3, u'L\'antépénultième semaine du mois'),
102
#            (-4, u'L\'anté-antépénultième semaine du mois'),
103
#            (-5, u'L\'anté-anté-antépénultième semaine du mois')
104

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

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

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

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

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

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

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

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

    
234

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

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

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

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

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

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

    
312
           limit - compute occurrences until limit days in the future
313

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

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

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

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

    
388
    def to_interval(self):
389
        return Interval(self.start_datetime, self.end_datetime)
390

    
391
    def is_event_absence(self):
392
        return False
393

    
394
    def get_missing_participants(self):
395
        missing_participants = []
396
        for participant in self.participants.all():
397
            holidays = None
398
            worker = participant.worker
399
            holidays = Holiday.objects.for_worker(worker) \
400
                  .for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
401
            if holidays:
402
                missing_participants.append(participant)
403
        return missing_participants
404

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

    
420
    WEEKDAY_DESRIPTION = [
421
            u'lundi',
422
            u'mardi',
423
            u'mercredi',
424
            u'jeudi',
425
            u'vendredi',
426
            u'samedi',
427
            u'dimanche'
428
    ]
429

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

    
447
    def __unicode__(self):
448
        return self.title
449

    
450
    def __repr__(self):
451
        return '<Event: on {start_datetime} with {participants}'.format(
452
                start_datetime=self.start_datetime,
453
                participants=self.participants.all() if self.id else '<un-saved>')
454

    
455

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

    
474

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

    
484

    
485
    @property
486
    def act(self):
487
        for act in self.act_set.all():
488
            if act.date == self.start_datetime.date():
489
                return act
490
        return self.build_act()
491

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

    
498
    def is_absent(self):
499
        act = self.act
500
        if act.id:
501
            return act.is_absent()
502
        return False
503

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

    
522
    def update_act(self, act):
523
        '''Update an act to match details of the meeting'''
524
        self.init_act(act)
525
        act.save()
526

    
527
    def init_act(self, act):
528
        delta = self.timedelta()
529
        duration = delta.seconds // 60
530
        act._duration = duration
531
        act.act_type = self.act_type
532
        act.patient = self.patient
533
        act.parent_event = self
534
        act.date = self.start_datetime.date()
535
        act.time = self.start_datetime.time()
536

    
537
    def save(self, *args, **kwargs):
538
        '''Force event_type to be patient meeting.'''
539
        self.event_type = EventType(id=1)
540
        super(EventWithAct, self).save(*args, **kwargs)
541

    
542
    def is_event_absence(self):
543
        return self.act.is_absent()
544

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

    
555

    
556
from django.db.models.signals import m2m_changed
557
from django.dispatch import receiver
558

    
559

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