Projet

Général

Profil

Télécharger (23 ko) Statistiques
| Branche: | Tag: | Révision:

calebasse / calebasse / agenda / models.py @ a01d85be

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

    
239

    
240
    def today_occurrence(self, today=None, match=False, upgrade=True):
241
        '''For a recurring event compute the today 'Event'.
242

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

    
295
    def next_occurence(self, today=None):
296
        '''Returns the next occurence after today.'''
297
        today = today or date.today()
298
        for occurence in self.all_occurences():
299
            if occurence.start_datetime.date() > today:
300
                return occurence
301

    
302
    def is_recurring(self):
303
        '''Is this event multiple ?'''
304
        return self.recurrence_periodicity is not None
305

    
306
    def get_exceptions_dict(self):
307
        if not hasattr(self, 'exceptions_dict'):
308
            self.exceptions_dict = dict()
309
            if self.exception_to_id is None:
310
                for exception in self.exceptions.all():
311
                    self.exceptions_dict[exception.exception_date] = exception
312
        return self.exceptions_dict
313

    
314
    def all_occurences(self, limit=90):
315
        '''Returns all occurences of this event as virtual Event objects
316

    
317
           limit - compute occurrences until limit days in the future
318

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

    
357
    def save(self, *args, **kwargs):
358
        assert self.recurrence_periodicity is None or self.exception_to is None
359
        assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
360
        assert self.start_datetime is not None
361
        self.sanitize() # init periodicity fields
362
        super(Event, self).save(*args, **kwargs)
363
        get_request().record('event-save', '{obj_id} saved by {user} from {ip}',
364
                             obj_id=self.id)
365
        self.acts_cleaning()
366

    
367
    def delete(self, *args, **kwargs):
368
        self.canceled = True
369
        # save will clean acts
370
        self.save(*args, **kwargs)
371

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

    
395
    def to_interval(self):
396
        return Interval(self.start_datetime, self.end_datetime)
397

    
398
    def is_event_absence(self):
399
        return False
400

    
401
    def get_inactive_participants(self):
402
        return self.participants.filter(worker__enabled=False)
403

    
404
    def get_missing_participants(self):
405
        missing_participants = []
406
        for participant in self.participants.all():
407
            holidays = None
408
            worker = participant.worker
409
            holidays = Holiday.objects.for_worker(worker) \
410
                  .for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
411
            if holidays:
412
                missing_participants.append(participant)
413
        return missing_participants
414

    
415
    RECURRENCE_DESCRIPTION = [
416
            u'Tous les %s',      #(1, None, None),
417
            u'Un %s sur deux',   #(2, None, None),
418
            u'Un %s sur trois',  #(3, None, None),
419
            u'Un %s sur quatre', #(4, None, None),
420
            u'Un %s sur cinq',   #(5, None, None),
421
            u'Le premier %s du mois',   #(None, 0, None),
422
            u'Le deuxième %s du mois',  #(None, 1, None),
423
            u'Le troisième %s du mois', #(None, 2, None),
424
            u'Le quatrième %s du mois', #(None, 3, None),
425
            u'Le dernier %s du mois',   #(None, 4, None),
426
            u'Les %s les semaines paires',    #(None, None, 0),
427
            u'Les %s les semaines impaires', #(None, None, 1)
428
    ]
429

    
430
    WEEKDAY_DESRIPTION = [
431
            u'lundi',
432
            u'mardi',
433
            u'mercredi',
434
            u'jeudi',
435
            u'vendredi',
436
            u'samedi',
437
            u'dimanche'
438
    ]
439

    
440
    def recurrence_description(self):
441
        '''Self description of this recurring event'''
442
        if not self.recurrence_periodicity:
443
            return None
444
        parts = []
445
        parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
446
            % self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
447
        if self.recurrence_end_date:
448
            parts.append(u'du')
449
        else:
450
            parts.append(u'à partir du')
451
        parts.append(self.start_datetime.strftime('%d/%m/%Y'))
452
        if self.recurrence_end_date:
453
            parts.append(u'au')
454
            parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
455
        return u' '.join(parts)
456

    
457
    def is_absent(self):
458
        try:
459
            return self.eventwithact.is_absent()
460
        except self.DoesNotExist:
461
            return False
462

    
463
    def __unicode__(self):
464
        return self.title
465

    
466
    def __repr__(self):
467
        return '<Event: on {start_datetime} with {participants}'.format(
468
                start_datetime=self.start_datetime,
469
                participants=self.participants.all() if self.id else '<un-saved>')
470

    
471

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

    
490

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

    
500

    
501
    @property
502
    def act(self):
503
        for act in self.act_set.all():
504
            if act.date == self.start_datetime.date():
505
                return act
506
        return self.build_act()
507

    
508
    def get_state(self):
509
        act = self.act
510
        if act.id:
511
            return act.get_state()
512
        return None
513

    
514
    def is_absent(self):
515
        act = self.act
516
        if act.id:
517
            return act.is_absent()
518
        return False
519

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

    
538
    def update_act(self, act):
539
        '''Update an act to match details of the meeting'''
540
        self.init_act(act)
541
        changes = {'delta': self.timedelta(), 'act_type': self.act_type,
542
                   'patient': self.patient, 'date': self.start_datetime.date(),
543
                   'time': self.start_datetime.time(), 'parent': self}
544
        get_request().record('act-update', '{obj_id} updated by {user} from {ip} with: {changes}', changes=changes)
545
        act.save()
546

    
547
    def init_act(self, act):
548
        delta = self.timedelta()
549
        duration = delta.seconds // 60
550
        act._duration = duration
551
        act.act_type = self.act_type
552
        act.patient = self.patient
553
        act.parent_event = self
554
        act.date = self.start_datetime.date()
555
        act.time = self.start_datetime.time()
556

    
557
    def save(self, *args, **kwargs):
558
        '''Force event_type to be patient meeting.'''
559
        self.event_type = EventType(id=1)
560
        super(EventWithAct, self).save(*args, **kwargs)
561
        get_request().record('eventwithact-save', '{obj_id} saved by {user} from {ip}', obj_id=self.id)
562

    
563
    def is_event_absence(self):
564
        return self.act.is_absent()
565

    
566
    def __unicode__(self):
567
        kwargs = {
568
                'patient': self.patient,
569
                'start_datetime': self.start_datetime,
570
                'act_type': self.act_type
571
        }
572
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
573
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
574
            '{act_type} ({act_type.id})'.format(**kwargs)
575

    
576

    
577
from django.db.models.signals import m2m_changed
578
from django.dispatch import receiver
579

    
580

    
581
@receiver(m2m_changed, sender=Event.participants.through)
582
def participants_changed(sender, instance, action, **kwargs):
583
    if action.startswith('post'):
584
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
585
        for act in instance.act_set.all():
586
            act.doctors = workers
(7-7/10)