Fix: plantage causé par un employé sans dossier
[auf_rh_dae.git] / project / rh / models.py
1 # -=- encoding: utf-8 -=-
2
3 import datetime
4 from datetime import date
5 from decimal import Decimal
6
7 import reversion
8 from auf.django.emploi.models import \
9 GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references
10 from auf.django.references import models as ref
11 from django.contrib.auth.models import User
12 from django.core.files.storage import FileSystemStorage
13 from django.core.exceptions import MultipleObjectsReturned
14 from django.db import models
15 from django.db.models import Q
16 from django.db.models.signals import post_save, pre_save
17 from django.conf import settings
18
19 from project.rh.change_list import \
20 RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \
21 STATUT_FUTUR
22 from project import groups
23 from project.rh.managers import (
24 PosteManager,
25 DossierManager,
26 EmployeManager,
27 DossierComparaisonManager,
28 PosteComparaisonManager,
29 ContratManager,
30 RemunerationManager,
31 ArchivableManager,
32 )
33
34
35 TWOPLACES = Decimal('0.01')
36
37 from project.rh.validators import validate_date_passee
38
39 # import pour relocaliser le modèle selon la convention (models.py pour
40 # introspection)
41 from project.rh.historique import ModificationTraite
42
43 # Constantes
44 HELP_TEXT_DATE = "format: jj-mm-aaaa"
45 REGIME_TRAVAIL_DEFAULT = Decimal('100.00')
46 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = Decimal('35.00')
47 REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT = \
48 "Saisir le nombre d'heure de travail à temps complet (100%), " \
49 "sans tenir compte du régime de travail"
50
51 # Upload de fichiers
52 storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
53 base_url=settings.PRIVE_MEDIA_URL)
54
55
56 class RemunIntegrityException(Exception):
57 pass
58
59 def poste_piece_dispatch(instance, filename):
60 path = "%s/poste/%s/%s" % (
61 instance._meta.app_label, instance.poste_id, filename
62 )
63 return path
64
65
66 def dossier_piece_dispatch(instance, filename):
67 path = "%s/dossier/%s/%s" % (
68 instance._meta.app_label, instance.dossier_id, filename
69 )
70 return path
71
72
73 def employe_piece_dispatch(instance, filename):
74 path = "%s/employe/%s/%s" % (
75 instance._meta.app_label, instance.employe_id, filename
76 )
77 return path
78
79
80 def contrat_dispatch(instance, filename):
81 path = "%s/contrat/%s/%s" % (
82 instance._meta.app_label, instance.dossier_id, filename
83 )
84 return path
85
86
87 class DateActiviteMixin(models.Model):
88 """
89 Mixin pour mettre à jour l'activité d'un modèle
90 """
91 class Meta:
92 abstract = True
93 date_creation = models.DateTimeField(auto_now_add=True,
94 null=True, blank=True,
95 verbose_name=u"Date de création",)
96 date_modification = models.DateTimeField(auto_now=True,
97 null=True, blank=True,
98 verbose_name=u"Date de modification",)
99
100
101 class Archivable(models.Model):
102 archive = models.BooleanField(u'archivé', default=False)
103
104 objects = ArchivableManager()
105 avec_archives = models.Manager()
106
107 class Meta:
108 abstract = True
109
110
111 class DevisableMixin(object):
112
113 def get_annee_pour_taux_devise(self):
114 return datetime.datetime.now().year
115
116 def taux_devise(self, devise=None):
117 if devise is None:
118 devise = self.devise
119
120 if devise is None:
121 return None
122 if devise.code == "EUR":
123 return 1
124
125 annee = self.get_annee_pour_taux_devise()
126 taux = TauxChange.objects.filter(devise=devise, annee__lte=annee) \
127 .order_by('-annee')
128 return taux[0].taux
129
130 def montant_euros_float(self):
131 try:
132 taux = self.taux_devise()
133 except Exception, e:
134 return e
135 if not taux:
136 return None
137 return float(self.montant) * float(taux)
138
139 def montant_euros(self):
140 return int(round(self.montant_euros_float(), 2))
141
142
143 class Commentaire(models.Model):
144 texte = models.TextField()
145 owner = models.ForeignKey(
146 'auth.User', db_column='owner', related_name='+',
147 verbose_name=u"Commentaire de"
148 )
149 date_creation = models.DateTimeField(
150 u'date', auto_now_add=True, blank=True, null=True
151 )
152
153 class Meta:
154 abstract = True
155 ordering = ['-date_creation']
156
157 def __unicode__(self):
158 return u'%s' % (self.texte)
159
160
161 ### POSTE
162
163 POSTE_APPEL_CHOICES = (
164 ('interne', 'Interne'),
165 ('externe', 'Externe'),
166 )
167
168
169 class Poste_( DateActiviteMixin, models.Model,):
170 """
171 Un Poste est un emploi (job) à combler dans une implantation.
172 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
173 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
174 """
175
176 objects = PosteManager()
177
178 # Identification
179 nom = models.CharField(u"Titre du poste", max_length=255)
180 nom_feminin = models.CharField(
181 u"Titre du poste (au féminin)", max_length=255, null=True
182 )
183 implantation = models.ForeignKey(
184 ref.Implantation,
185 help_text=u"Taper le nom de l'implantation ou sa région",
186 db_column='implantation', related_name='+'
187 )
188 type_poste = models.ForeignKey(
189 'TypePoste', db_column='type_poste',
190 help_text=u"Taper le nom du type de poste", related_name='+',
191 null=True, verbose_name=u"type de poste"
192 )
193 service = models.ForeignKey(
194 'Service', db_column='service', related_name='%(app_label)s_postes',
195 verbose_name=u"direction/service/pôle support", null=True
196 )
197 responsable = models.ForeignKey(
198 'Poste', db_column='responsable',
199 related_name='+', null=True,
200 help_text=u"Taper le nom du poste ou du type de poste",
201 verbose_name=u"Poste du responsable"
202 )
203
204 # Contrat
205 regime_travail = models.DecimalField(
206 u"temps de travail", max_digits=12, decimal_places=2,
207 default=REGIME_TRAVAIL_DEFAULT, null=True,
208 help_text="% du temps complet"
209 )
210 regime_travail_nb_heure_semaine = models.DecimalField(
211 u"nb. heures par semaine", max_digits=12, decimal_places=2,
212 null=True, default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
213 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
214 )
215
216 # Recrutement
217 local = models.NullBooleanField(
218 u"local", default=True, null=True, blank=True
219 )
220 expatrie = models.NullBooleanField(
221 u"expatrié", default=False, null=True, blank=True
222 )
223 mise_a_disposition = models.NullBooleanField(
224 u"mise à disposition", null=True, default=False
225 )
226 appel = models.CharField(
227 u"Appel à candidature", max_length=10, null=True,
228 choices=POSTE_APPEL_CHOICES, default='interne'
229 )
230
231 # Rémunération
232 classement_min = models.ForeignKey(
233 'Classement', db_column='classement_min', related_name='+',
234 null=True, blank=True
235 )
236 classement_max = models.ForeignKey(
237 'Classement', db_column='classement_max', related_name='+',
238 null=True, blank=True
239 )
240 valeur_point_min = models.ForeignKey(
241 'ValeurPoint',
242 help_text=u"Taper le code ou le nom de l'implantation",
243 db_column='valeur_point_min', related_name='+', null=True,
244 blank=True
245 )
246 valeur_point_max = models.ForeignKey(
247 'ValeurPoint',
248 help_text=u"Taper le code ou le nom de l'implantation",
249 db_column='valeur_point_max', related_name='+', null=True,
250 blank=True
251 )
252 devise_min = models.ForeignKey(
253 'Devise', db_column='devise_min', null=True, related_name='+'
254 )
255 devise_max = models.ForeignKey(
256 'Devise', db_column='devise_max', null=True, related_name='+'
257 )
258 salaire_min = models.DecimalField(
259 max_digits=12, decimal_places=2, default=0,
260 )
261 salaire_max = models.DecimalField(
262 max_digits=12, decimal_places=2, default=0,
263 )
264 indemn_min = models.DecimalField(
265 max_digits=12, decimal_places=2, default=0,
266 )
267 indemn_max = models.DecimalField(
268 max_digits=12, decimal_places=2, default=0,
269 )
270 autre_min = models.DecimalField(
271 max_digits=12, decimal_places=2, default=0,
272 )
273 autre_max = models.DecimalField(
274 max_digits=12, decimal_places=2, default=0,
275 )
276
277 # Comparatifs de rémunération
278 devise_comparaison = models.ForeignKey(
279 'Devise', null=True, blank=True, db_column='devise_comparaison',
280 related_name='+'
281 )
282 comp_locale_min = models.DecimalField(
283 max_digits=12, decimal_places=2, null=True, blank=True
284 )
285 comp_locale_max = models.DecimalField(
286 max_digits=12, decimal_places=2, null=True, blank=True
287 )
288 comp_universite_min = models.DecimalField(
289 max_digits=12, decimal_places=2, null=True, blank=True
290 )
291 comp_universite_max = models.DecimalField(
292 max_digits=12, decimal_places=2, null=True, blank=True
293 )
294 comp_fonctionpub_min = models.DecimalField(
295 max_digits=12, decimal_places=2, null=True, blank=True
296 )
297 comp_fonctionpub_max = models.DecimalField(
298 max_digits=12, decimal_places=2, null=True, blank=True
299 )
300 comp_ong_min = models.DecimalField(
301 max_digits=12, decimal_places=2, null=True, blank=True
302 )
303 comp_ong_max = models.DecimalField(
304 max_digits=12, decimal_places=2, null=True, blank=True
305 )
306 comp_autre_min = models.DecimalField(
307 max_digits=12, decimal_places=2, null=True, blank=True
308 )
309 comp_autre_max = models.DecimalField(
310 max_digits=12, decimal_places=2, null=True, blank=True
311 )
312
313 # Justification
314 justification = models.TextField(null=True, blank=True)
315
316 # Autres Metadata
317 date_debut = models.DateField(
318 u"date de début", help_text=HELP_TEXT_DATE, null=True, blank=True,
319 db_index=True
320 )
321 date_fin = models.DateField(
322 u"date de fin", help_text=HELP_TEXT_DATE, null=True, blank=True,
323 db_index=True
324 )
325
326 class Meta:
327 abstract = True
328 ordering = ['implantation__nom', 'nom']
329 verbose_name = u"Poste"
330 verbose_name_plural = u"Postes"
331 ordering = ["nom"]
332
333 def __unicode__(self):
334 representation = u'%s - %s [%s]' % (
335 self.implantation, self.nom, self.id
336 )
337 return representation
338
339 prefix_implantation = "implantation__zone_administrative"
340
341 def get_zones_administratives(self):
342 return [self.implantation.zone_administrative]
343
344 def get_devise(self):
345 vp = ValeurPoint.objects.filter(
346 implantation=self.implantation, devise__archive=False
347 ).order_by('annee')
348 if len(vp) > 0:
349 return vp[0].devise
350 else:
351 return Devise.objects.get(code='EUR')
352
353
354 class Poste(Poste_):
355 __doc__ = Poste_.__doc__
356
357 # meta dématérialisation : pour permettre le filtrage
358 vacant = models.NullBooleanField(u"vacant", null=True, blank=True)
359
360 def is_vacant(self):
361 vacant = True
362 if self.occupe_par():
363 vacant = False
364 return vacant
365
366 def occupe_par(self):
367 """
368 Retourne la liste d'employé occupant ce poste.
369 Généralement, retourne une liste d'un élément.
370 Si poste inoccupé, retourne liste vide.
371 UTILISE pour mettre a jour le flag vacant
372 """
373 return [
374 d.employe
375 for d in self.rh_dossiers.exclude(date_fin__lt=date.today())
376 ]
377
378 reversion.register(Poste, format='xml', follow=[
379 'rh_financements', 'rh_pieces', 'rh_comparaisons_internes',
380 'commentaires'
381 ])
382
383
384 POSTE_FINANCEMENT_CHOICES = (
385 ('A', 'A - Frais de personnel'),
386 ('B', 'B - Projet(s)-Titre(s)'),
387 ('C', 'C - Autre')
388 )
389
390
391 class PosteFinancement_(models.Model):
392 """
393 Pour un Poste, structure d'informations décrivant comment on prévoit
394 financer ce Poste.
395 """
396 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
397 pourcentage = models.DecimalField(
398 max_digits=12, decimal_places=2,
399 help_text="ex.: 33.33 % (décimale avec point)"
400 )
401 commentaire = models.TextField(
402 help_text="Spécifiez la source de financement."
403 )
404
405 class Meta:
406 abstract = True
407 ordering = ['type']
408
409 def __unicode__(self):
410 return u'%s : %s %%' % (self.type, self.pourcentage)
411
412 def choix(self):
413 return u"%s" % dict(POSTE_FINANCEMENT_CHOICES)[self.type]
414
415
416 class PosteFinancement(PosteFinancement_):
417 poste = models.ForeignKey(
418 Poste, db_column='poste', related_name='rh_financements'
419 )
420
421 reversion.register(PosteFinancement, format='xml')
422
423
424 class PostePiece_(models.Model):
425 """
426 Documents relatifs au Poste.
427 Ex.: Description de poste
428 """
429 nom = models.CharField(u"Nom", max_length=255)
430 fichier = models.FileField(
431 u"Fichier", upload_to=poste_piece_dispatch, storage=storage_prive,
432 max_length=255
433 )
434
435 class Meta:
436 abstract = True
437 ordering = ['nom']
438
439 def __unicode__(self):
440 return u'%s' % (self.nom)
441
442
443 class PostePiece(PostePiece_):
444 poste = models.ForeignKey(
445 Poste, db_column='poste', related_name='rh_pieces'
446 )
447
448 reversion.register(PostePiece, format='xml')
449
450
451 class PosteComparaison_(models.Model, DevisableMixin):
452 """
453 De la même manière qu'un dossier, un poste peut-être comparé à un autre
454 poste.
455 """
456 objects = PosteComparaisonManager()
457
458 implantation = models.ForeignKey(
459 ref.Implantation, null=True, blank=True, related_name="+"
460 )
461 nom = models.CharField(u"Poste", max_length=255, null=True, blank=True)
462 montant = models.IntegerField(null=True)
463 devise = models.ForeignKey(
464 "Devise", related_name='+', null=True, blank=True
465 )
466
467 class Meta:
468 abstract = True
469
470 def __unicode__(self):
471 return self.nom
472
473
474 class PosteComparaison(PosteComparaison_):
475 poste = models.ForeignKey(
476 Poste, related_name='rh_comparaisons_internes'
477 )
478
479 reversion.register(PosteComparaison, format='xml')
480
481
482 class PosteCommentaire(Commentaire):
483 poste = models.ForeignKey(
484 Poste, db_column='poste', related_name='commentaires'
485 )
486
487 reversion.register(PosteCommentaire, format='xml')
488
489 ### EMPLOYÉ/PERSONNE
490
491 class Employe(models.Model):
492 """
493 Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
494 Dossiers qu'il occupe ou a occupé de Postes.
495
496 Cette classe aurait pu avantageusement s'appeler Personne car la notion
497 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
498 """
499
500 objects = EmployeManager()
501
502 # Identification
503 nom = models.CharField(max_length=255)
504 prenom = models.CharField(u"prénom", max_length=255)
505 nom_affichage = models.CharField(
506 u"nom d'affichage", max_length=255, null=True, blank=True
507 )
508 nationalite = models.ForeignKey(
509 ref.Pays, to_field='code', db_column='nationalite',
510 related_name='employes_nationalite', verbose_name=u"nationalité",
511 blank=True, null=True
512 )
513 date_naissance = models.DateField(
514 u"date de naissance", help_text=HELP_TEXT_DATE,
515 validators=[validate_date_passee], null=True, blank=True
516 )
517 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
518
519 # Infos personnelles
520 situation_famille = models.CharField(
521 u"situation familiale", max_length=1, choices=SITUATION_CHOICES,
522 null=True, blank=True
523 )
524 date_entree = models.DateField(
525 u"date d'entrée à l'AUF", help_text=HELP_TEXT_DATE, null=True,
526 blank=True
527 )
528
529 # Coordonnées
530 tel_domicile = models.CharField(
531 u"tél. domicile", max_length=255, null=True, blank=True
532 )
533 tel_cellulaire = models.CharField(
534 u"tél. cellulaire", max_length=255, null=True, blank=True
535 )
536 adresse = models.CharField(max_length=255, null=True, blank=True)
537 ville = models.CharField(max_length=255, null=True, blank=True)
538 province = models.CharField(max_length=255, null=True, blank=True)
539 code_postal = models.CharField(max_length=255, null=True, blank=True)
540 pays = models.ForeignKey(
541 ref.Pays, to_field='code', db_column='pays',
542 related_name='employes', null=True, blank=True
543 )
544 courriel_perso = models.EmailField(
545 u'adresse courriel personnelle', blank=True
546 )
547
548 # meta dématérialisation : pour permettre le filtrage
549 nb_postes = models.IntegerField(u"nombre de postes", null=True, blank=True)
550
551 class Meta:
552 ordering = ['nom', 'prenom']
553 verbose_name = u"Employé"
554 verbose_name_plural = u"Employés"
555
556 def __unicode__(self):
557 return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
558
559 def get_latest_dossier_ordered_by_date_fin_and_principal(self):
560 res = self.rh_dossiers.order_by(
561 '-principal', 'date_fin')
562
563 # Retourne en le premier du queryset si la date de fin est None
564 # Sinon, retourne le plus récent selon la date de fin.
565 try:
566 first = res[0]
567 except IndexError:
568 return None
569 if first.date_fin == None:
570 return first
571 else:
572 return res.order_by('-principal', '-date_fin')[0]
573
574 def civilite(self):
575 civilite = u''
576 if self.genre.upper() == u'M':
577 civilite = u'M.'
578 elif self.genre.upper() == u'F':
579 civilite = u'Mme'
580 return civilite
581
582 def url_photo(self):
583 """
584 Retourne l'URL du service retournant la photo de l'Employe.
585 Équivalent reverse url 'rh_photo' avec id en param.
586 """
587 from django.core.urlresolvers import reverse
588 return reverse('rh_photo', kwargs={'id': self.id})
589
590 def dossiers_passes(self):
591 params = {KEY_STATUT: STATUT_INACTIF, }
592 search = RechercheTemporelle(params, Dossier)
593 search.purge_params(params)
594 q = search.get_q_temporel(self.rh_dossiers)
595 return self.rh_dossiers.filter(q)
596
597 def dossiers_futurs(self):
598 params = {KEY_STATUT: STATUT_FUTUR, }
599 search = RechercheTemporelle(params, Dossier)
600 search.purge_params(params)
601 q = search.get_q_temporel(self.rh_dossiers)
602 return self.rh_dossiers.filter(q)
603
604 def dossiers_encours(self):
605 params = {KEY_STATUT: STATUT_ACTIF, }
606 search = RechercheTemporelle(params, Dossier)
607 search.purge_params(params)
608 q = search.get_q_temporel(self.rh_dossiers)
609 return self.rh_dossiers.filter(q)
610
611 def dossier_principal_pour_annee(self):
612 return self.dossier_principal(pour_annee=True)
613
614 def dossier_principal(self, pour_annee=False):
615 """
616 Retourne le dossier principal (ou le plus ancien si il y en a
617 plusieurs)
618
619 Si pour_annee == True, retourne le ou les dossiers principaux
620 pour l'annee en cours, sinon, le ou les dossiers principaux
621 pour la journee en cours.
622
623 TODO: (Refactoring possible): Utiliser meme logique dans
624 dae/templatetags/dae.py
625 """
626
627 today = date.today()
628 if pour_annee:
629 year = today.year
630 year_start = date(year, 1, 1)
631 year_end = date(year, 12, 31)
632
633 try:
634 dossier = self.rh_dossiers.filter(
635 (Q(date_debut__lte=year_end, date_fin__isnull=True) |
636 Q(date_debut__isnull=True, date_fin__gte=year_start) |
637 Q(date_debut__lte=year_end, date_fin__gte=year_start) |
638 Q(date_debut__isnull=True, date_fin__isnull=True)) &
639 Q(principal=True)).order_by('date_debut')[0]
640 except IndexError, Dossier.DoesNotExist:
641 dossier = None
642 return dossier
643 else:
644 try:
645 dossier = self.rh_dossiers.filter(
646 (Q(date_debut__lte=today, date_fin__isnull=True) |
647 Q(date_debut__isnull=True, date_fin__gte=today) |
648 Q(date_debut__lte=today, date_fin__gte=today) |
649 Q(date_debut__isnull=True, date_fin__isnull=True)) &
650 Q(principal=True)).order_by('date_debut')[0]
651 except IndexError, Dossier.DoesNotExist:
652 dossier = None
653 return dossier
654
655
656 def postes_encours(self):
657 postes_encours = set()
658 for d in self.dossiers_encours():
659 postes_encours.add(d.poste)
660 return postes_encours
661
662 def poste_principal(self):
663 """
664 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
665 Idée derrière :
666 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
667 """
668 # DEPRECATED : on a maintenant Dossier.principal
669 poste = Poste.objects.none()
670 try:
671 poste = self.dossiers_encours().order_by('date_debut')[0].poste
672 except:
673 pass
674 return poste
675
676 prefix_implantation = \
677 "rh_dossiers__poste__implantation__zone_administrative"
678
679 def get_zones_administratives(self):
680 return [
681 d.poste.implantation.zone_administrative
682 for d in self.dossiers.all()
683 ]
684
685 reversion.register(Employe, format='xml', follow=[
686 'pieces', 'commentaires', 'ayantdroits'
687 ])
688
689
690 class EmployePiece(models.Model):
691 """
692 Documents relatifs à un employé.
693 Ex.: CV...
694 """
695 employe = models.ForeignKey(
696 'Employe', db_column='employe', related_name="pieces",
697 verbose_name=u"employé"
698 )
699 nom = models.CharField(max_length=255)
700 fichier = models.FileField(
701 u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive,
702 max_length=255
703 )
704
705 class Meta:
706 ordering = ['nom']
707 verbose_name = u"Employé pièce"
708 verbose_name_plural = u"Employé pièces"
709
710 def __unicode__(self):
711 return u'%s' % (self.nom)
712
713 reversion.register(EmployePiece, format='xml')
714
715
716 class EmployeCommentaire(Commentaire):
717 employe = models.ForeignKey(
718 'Employe', db_column='employe', related_name='commentaires'
719 )
720
721 class Meta:
722 verbose_name = u"Employé commentaire"
723 verbose_name_plural = u"Employé commentaires"
724
725 reversion.register(EmployeCommentaire, format='xml')
726
727
728 LIEN_PARENTE_CHOICES = (
729 ('Conjoint', 'Conjoint'),
730 ('Conjointe', 'Conjointe'),
731 ('Fille', 'Fille'),
732 ('Fils', 'Fils'),
733 )
734
735
736 class AyantDroit(models.Model):
737 """
738 Personne en relation avec un Employe.
739 """
740 # Identification
741 nom = models.CharField(max_length=255)
742 prenom = models.CharField(u"prénom", max_length=255)
743 nom_affichage = models.CharField(
744 u"nom d'affichage", max_length=255, null=True, blank=True
745 )
746 nationalite = models.ForeignKey(
747 ref.Pays, to_field='code', db_column='nationalite',
748 related_name='ayantdroits_nationalite',
749 verbose_name=u"nationalité", null=True, blank=True
750 )
751 date_naissance = models.DateField(
752 u"Date de naissance", help_text=HELP_TEXT_DATE,
753 validators=[validate_date_passee], null=True, blank=True
754 )
755 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
756
757 # Relation
758 employe = models.ForeignKey(
759 'Employe', db_column='employe', related_name='ayantdroits',
760 verbose_name=u"Employé"
761 )
762 lien_parente = models.CharField(
763 u"lien de parenté", max_length=10, choices=LIEN_PARENTE_CHOICES,
764 null=True, blank=True
765 )
766
767 class Meta:
768 ordering = ['nom', ]
769 verbose_name = u"Ayant droit"
770 verbose_name_plural = u"Ayants droit"
771
772 def __unicode__(self):
773 return u'%s %s' % (self.nom.upper(), self.prenom, )
774
775 prefix_implantation = \
776 "employe__dossiers__poste__implantation__zone_administrative"
777
778 def get_zones_administratives(self):
779 return [
780 d.poste.implantation.zone_administrative
781 for d in self.employe.dossiers.all()
782 ]
783
784 reversion.register(AyantDroit, format='xml', follow=['commentaires'])
785
786
787 class AyantDroitCommentaire(Commentaire):
788 ayant_droit = models.ForeignKey(
789 'AyantDroit', db_column='ayant_droit', related_name='commentaires'
790 )
791
792 reversion.register(AyantDroitCommentaire, format='xml')
793
794
795 ### DOSSIER
796
797 STATUT_RESIDENCE_CHOICES = (
798 ('local', 'Local'),
799 ('expat', 'Expatrié'),
800 )
801
802 COMPTE_COMPTA_CHOICES = (
803 ('coda', 'CODA'),
804 ('scs', 'SCS'),
805 ('aucun', 'Aucun'),
806 )
807
808
809 class Dossier_(DateActiviteMixin, models.Model, DevisableMixin,):
810 """
811 Le Dossier regroupe les informations relatives à l'occupation
812 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
813 par un Employe.
814
815 Plusieurs Contrats peuvent être associés au Dossier.
816 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
817 lequel aucun Dossier n'existe est un poste vacant.
818 """
819
820 objects = DossierManager()
821
822 # TODO: OneToOne ??
823 statut = models.ForeignKey('Statut', related_name='+', null=True)
824 organisme_bstg = models.ForeignKey(
825 'OrganismeBstg', db_column='organisme_bstg', related_name='+',
826 verbose_name=u"organisme",
827 help_text=(
828 u"Si détaché (DET) ou mis à disposition (MAD), "
829 u"préciser l'organisme."
830 ), null=True, blank=True
831 )
832
833 # Recrutement
834 remplacement = models.BooleanField(default=False)
835 remplacement_de = models.ForeignKey(
836 'self', related_name='+', help_text=u"Taper le nom de l'employé",
837 null=True, blank=True
838 )
839 statut_residence = models.CharField(
840 u"statut", max_length=10, default='local', null=True,
841 choices=STATUT_RESIDENCE_CHOICES
842 )
843
844 # Rémunération
845 classement = models.ForeignKey(
846 'Classement', db_column='classement', related_name='+', null=True,
847 blank=True
848 )
849 regime_travail = models.DecimalField(
850 u"régime de travail", max_digits=12, null=True, decimal_places=2,
851 default=REGIME_TRAVAIL_DEFAULT, help_text="% du temps complet"
852 )
853 regime_travail_nb_heure_semaine = models.DecimalField(
854 u"nb. heures par semaine", max_digits=12,
855 decimal_places=2, null=True,
856 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
857 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
858 )
859
860 # Occupation du Poste par cet Employe (anciennement "mandat")
861 date_debut = models.DateField(
862 u"date de début d'occupation de poste", db_index=True
863 )
864 date_fin = models.DateField(
865 u"Date de fin d'occupation de poste", null=True, blank=True,
866 db_index=True
867 )
868
869 # Meta-data:
870 est_cadre = models.BooleanField(
871 u"Est un cadre?",
872 default=False,
873 )
874
875 # Comptes
876 compte_compta = models.CharField(max_length=10, default='aucun',
877 verbose_name=u'Compte comptabilité',
878 choices=COMPTE_COMPTA_CHOICES)
879 compte_courriel = models.BooleanField()
880
881 class Meta:
882 abstract = True
883 ordering = ['employe__nom', ]
884 verbose_name = u"Dossier"
885 verbose_name_plural = "Dossiers"
886
887 def salaire_theorique(self):
888 annee = date.today().year
889 coeff = self.classement.coefficient
890 implantation = self.poste.implantation
891 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
892
893 montant = coeff * point.valeur
894 devise = point.devise
895 return {'montant': montant, 'devise': devise}
896
897 def __unicode__(self):
898 poste = self.poste.nom
899 if self.employe.genre == 'F':
900 poste = self.poste.nom_feminin
901 return u'%s - %s' % (self.employe, poste)
902
903 prefix_implantation = "poste__implantation__zone_administrative"
904
905 def get_zones_administratives(self):
906 return [self.poste.implantation.zone_administrative]
907
908 def remunerations(self):
909 key = "%s_remunerations" % self._meta.app_label
910 remunerations = getattr(self, key)
911 return remunerations.all().order_by('-date_debut')
912
913 def remunerations_en_cours(self):
914 q = Q(date_fin__exact=None) | Q(date_fin__gt=datetime.date.today())
915 return self.remunerations().all().filter(q).order_by('date_debut')
916
917 def get_salaire(self):
918 try:
919 return [r for r in self.remunerations().order_by('-date_debut')
920 if r.type_id == 1][0]
921 except:
922 return None
923
924 def get_salaire_euros(self):
925 tx = self.taux_devise()
926 return (float)(tx) * (float)(self.salaire)
927
928 def get_remunerations_brutes(self):
929 """
930 1 Salaire de base
931 3 Indemnité de base
932 4 Indemnité d'expatriation
933 5 Indemnité pour frais
934 6 Indemnité de logement
935 7 Indemnité de fonction
936 8 Indemnité de responsabilité
937 9 Indemnité de transport
938 10 Indemnité compensatrice
939 11 Indemnité de subsistance
940 12 Indemnité différentielle
941 13 Prime d'installation
942 14 Billet d'avion
943 15 Déménagement
944 16 Indemnité de départ
945 18 Prime de 13ième mois
946 19 Prime d'intérim
947 """
948 ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
949 return [r for r in self.remunerations_en_cours().all()
950 if r.type_id in ids]
951
952 def get_charges_salariales(self):
953 """
954 20 Charges salariales ?
955 """
956 ids = [20]
957 return [r for r in self.remunerations_en_cours().all()
958 if r.type_id in ids]
959
960 def get_charges_patronales(self):
961 """
962 17 Charges patronales
963 """
964 ids = [17]
965 return [r for r in self.remunerations_en_cours().all()
966 if r.type_id in ids]
967
968 def get_remunerations_tierces(self):
969 """
970 2 Salaire MAD
971 """
972 return [r for r in self.remunerations_en_cours().all()
973 if r.type_id in (2,)]
974
975 # DEVISE LOCALE
976
977 def get_total_local_charges_salariales(self):
978 devise = self.poste.get_devise()
979 total = 0.0
980 for r in self.get_charges_salariales():
981 if r.devise != devise:
982 return None
983 total += float(r.montant)
984 return total
985
986 def get_total_local_charges_patronales(self):
987 devise = self.poste.get_devise()
988 total = 0.0
989 for r in self.get_charges_patronales():
990 if r.devise != devise:
991 return None
992 total += float(r.montant)
993 return total
994
995 def get_local_salaire_brut(self):
996 """
997 somme des rémuérations brutes
998 """
999 devise = self.poste.get_devise()
1000 total = 0.0
1001 for r in self.get_remunerations_brutes():
1002 if r.devise != devise:
1003 return None
1004 total += float(r.montant)
1005 return total
1006
1007 def get_local_salaire_net(self):
1008 """
1009 salaire brut - charges salariales
1010 """
1011 devise = self.poste.get_devise()
1012 total_charges = 0.0
1013 for r in self.get_charges_salariales():
1014 if r.devise != devise:
1015 return None
1016 total_charges += float(r.montant)
1017 return self.get_local_salaire_brut() - total_charges
1018
1019 def get_local_couts_auf(self):
1020 """
1021 salaire net + charges patronales
1022 """
1023 devise = self.poste.get_devise()
1024 total_charges = 0.0
1025 for r in self.get_charges_patronales():
1026 if r.devise != devise:
1027 return None
1028 total_charges += float(r.montant)
1029 return self.get_local_salaire_net() + total_charges
1030
1031 def get_total_local_remunerations_tierces(self):
1032 devise = self.poste.get_devise()
1033 total = 0.0
1034 for r in self.get_remunerations_tierces():
1035 if r.devise != devise:
1036 return None
1037 total += float(r.montant)
1038 return total
1039
1040 # DEVISE EURO
1041
1042 def get_total_charges_salariales(self):
1043 total = 0.0
1044 for r in self.get_charges_salariales():
1045 total += r.montant_euros()
1046 return total
1047
1048 def get_total_charges_patronales(self):
1049 total = 0.0
1050 for r in self.get_charges_patronales():
1051 total += r.montant_euros()
1052 return total
1053
1054 def get_salaire_brut(self):
1055 """
1056 somme des rémuérations brutes
1057 """
1058 total = 0.0
1059 for r in self.get_remunerations_brutes():
1060 total += r.montant_euros()
1061 return total
1062
1063 def get_salaire_net(self):
1064 """
1065 salaire brut - charges salariales
1066 """
1067 total_charges = 0.0
1068 for r in self.get_charges_salariales():
1069 total_charges += r.montant_euros()
1070 return self.get_salaire_brut() - total_charges
1071
1072 def get_couts_auf(self):
1073 """
1074 salaire net + charges patronales
1075 """
1076 total_charges = 0.0
1077 for r in self.get_charges_patronales():
1078 total_charges += r.montant_euros()
1079 return self.get_salaire_net() + total_charges
1080
1081 def get_total_remunerations_tierces(self):
1082 total = 0.0
1083 for r in self.get_remunerations_tierces():
1084 total += r.montant_euros()
1085 return total
1086
1087 def premier_contrat(self):
1088 """contrat avec plus petite date de début"""
1089 try:
1090 contrat = self.rh_contrats.exclude(date_debut=None) \
1091 .order_by('date_debut')[0]
1092 except IndexError, Contrat.DoesNotExist:
1093 contrat = None
1094 return contrat
1095
1096 def dernier_contrat(self):
1097 """contrat avec plus grande date de fin"""
1098 try:
1099 contrat = self.rh_contrats.exclude(date_debut=None) \
1100 .order_by('-date_debut')[0]
1101 except IndexError, Contrat.DoesNotExist:
1102 contrat = None
1103 return contrat
1104
1105 def actif(self):
1106 today = date.today()
1107 return (self.date_debut is None or self.date_debut <= today) \
1108 and (self.date_fin is None or self.date_fin >= today) \
1109 and not (self.date_fin is None and self.date_debut is None)
1110
1111
1112 class Dossier(Dossier_):
1113 __doc__ = Dossier_.__doc__
1114 poste = models.ForeignKey(
1115 Poste, db_column='poste', related_name='rh_dossiers',
1116 help_text=u"Taper le nom du poste ou du type de poste",
1117 )
1118 employe = models.ForeignKey(
1119 'Employe', db_column='employe',
1120 help_text=u"Taper le nom de l'employé",
1121 related_name='rh_dossiers', verbose_name=u"employé"
1122 )
1123 principal = models.BooleanField(
1124 u"dossier principal", default=True,
1125 help_text=(
1126 u"Ce dossier est pour le principal poste occupé par l'employé"
1127 )
1128 )
1129
1130
1131 reversion.register(Dossier, format='xml', follow=[
1132 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
1133 'rh_contrats', 'commentaires'
1134 ])
1135
1136
1137 class RHDossierClassementRecord(models.Model):
1138 classement = models.ForeignKey(
1139 'Classement',
1140 related_name='classement_records',
1141 )
1142 dossier = models.ForeignKey(
1143 'Dossier',
1144 related_name='classement_records',
1145 )
1146 date_debut = models.DateField(
1147 u"date de début",
1148 help_text=HELP_TEXT_DATE,
1149 null=True,
1150 blank=True,
1151 db_index=True
1152 )
1153 date_fin = models.DateField(
1154 u"date de fin",
1155 help_text=HELP_TEXT_DATE,
1156 null=True,
1157 blank=True,
1158 db_index=True
1159 )
1160 commentaire = models.CharField(
1161 max_length=2048,
1162 blank=True,
1163 null=True,
1164 default='',
1165 )
1166
1167 def __unicode__(self):
1168 return self.classement.__unicode__()
1169
1170 class Meta:
1171 verbose_name = u"Element d'historique de classement"
1172 verbose_name_plural = u"Historique de classement"
1173
1174 @classmethod
1175 def post_save_handler(cls,
1176 sender,
1177 instance,
1178 created,
1179 using,
1180 **kw):
1181
1182 today = date.today()
1183 previous_record = None
1184 previous_classement = None
1185 has_changed = False
1186
1187 # Premièrement, pour les nouvelles instances:
1188 if created:
1189 if not instance.classement:
1190 return
1191 else:
1192 cls.objects.create(
1193 date_debut=instance.date_debut,
1194 classement=instance.classement,
1195 dossier=instance,
1196 )
1197 return
1198
1199 # Deuxièmement, pour les instances existantes:
1200
1201 # Détermine si:
1202 # 1. Est-ce que le classement a changé?
1203 # 2. Est-ce qu'une historique de classement existe déjà
1204 try:
1205 previous_record = cls.objects.get(
1206 dossier=instance,
1207 classement=instance.before_save.classement,
1208 date_fin=None,
1209 )
1210 except cls.DoesNotExist:
1211 if instance.before_save.classement:
1212 # Il était censé avoir une historique de classement
1213 # donc on le créé.
1214 previous_record = cls.objects.create(
1215 date_debut=instance.before_save.date_debut,
1216 classement=instance.before_save.classement,
1217 dossier=instance,
1218 )
1219 previous_classement = instance.before_save.classement
1220 except MultipleObjectsReturned:
1221 qs = cls.objects.filter(
1222 dossier=instance,
1223 classement=instance.before_save.classement,
1224 date_fin=None,
1225 )
1226 latest = qs.latest('date_debut')
1227 qs.exclude(id=latest.id).update(date_fin=today)
1228 previous_record = latest
1229 previous_classement = latest.classement
1230 else:
1231 previous_classement = previous_record.classement
1232
1233 has_changed = (
1234 instance.classement !=
1235 previous_classement
1236 )
1237
1238 # Cas aucun changement:
1239 if not has_changed:
1240 return
1241
1242 else:
1243 # Classement a changé
1244 if previous_record:
1245 previous_record.date_fin = today
1246 previous_record.save()
1247
1248 if instance.classement:
1249 cls.objects.create(
1250 date_debut=today,
1251 classement=instance.classement,
1252 dossier=instance,
1253 )
1254
1255
1256 class DossierPiece_(models.Model):
1257 """
1258 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
1259 Ex.: Lettre de motivation.
1260 """
1261 nom = models.CharField(max_length=255)
1262 fichier = models.FileField(
1263 upload_to=dossier_piece_dispatch, storage=storage_prive,
1264 max_length=255
1265 )
1266
1267 class Meta:
1268 abstract = True
1269 ordering = ['nom']
1270
1271 def __unicode__(self):
1272 return u'%s' % (self.nom)
1273
1274
1275 class DossierPiece(DossierPiece_):
1276 dossier = models.ForeignKey(
1277 Dossier, db_column='dossier', related_name='rh_dossierpieces'
1278 )
1279
1280 reversion.register(DossierPiece, format='xml')
1281
1282 class DossierCommentaire(Commentaire):
1283 dossier = models.ForeignKey(
1284 Dossier, db_column='dossier', related_name='commentaires'
1285 )
1286
1287 reversion.register(DossierCommentaire, format='xml')
1288
1289
1290 class DossierComparaison_(models.Model, DevisableMixin):
1291 """
1292 Photo d'une comparaison salariale au moment de l'embauche.
1293 """
1294 objects = DossierComparaisonManager()
1295
1296 implantation = models.ForeignKey(
1297 ref.Implantation, related_name="+", null=True, blank=True
1298 )
1299 poste = models.CharField(max_length=255, null=True, blank=True)
1300 personne = models.CharField(max_length=255, null=True, blank=True)
1301 montant = models.IntegerField(null=True)
1302 devise = models.ForeignKey(
1303 'Devise', related_name='+', null=True, blank=True
1304 )
1305
1306 class Meta:
1307 abstract = True
1308
1309 def __unicode__(self):
1310 return "%s (%s)" % (self.poste, self.personne)
1311
1312
1313 class DossierComparaison(DossierComparaison_):
1314 dossier = models.ForeignKey(
1315 Dossier, related_name='rh_comparaisons'
1316 )
1317
1318 reversion.register(DossierComparaison, format='xml')
1319
1320
1321 ### RÉMUNÉRATION
1322
1323 class RemunerationMixin(models.Model):
1324
1325 # Identification
1326 type = models.ForeignKey(
1327 'TypeRemuneration', db_column='type', related_name='+',
1328 verbose_name=u"type de rémunération"
1329 )
1330 type_revalorisation = models.ForeignKey(
1331 'TypeRevalorisation', db_column='type_revalorisation',
1332 related_name='+', verbose_name=u"type de revalorisation",
1333 null=True, blank=True
1334 )
1335 montant = models.DecimalField(
1336 null=True, blank=True, max_digits=12, decimal_places=2
1337 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1338 devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
1339
1340 # commentaire = precision
1341 commentaire = models.CharField(max_length=255, null=True, blank=True)
1342
1343 # date_debut = anciennement date_effectif
1344 date_debut = models.DateField(
1345 u"date de début", null=True, blank=True, db_index=True
1346 )
1347 date_fin = models.DateField(
1348 u"date de fin", null=True, blank=True, db_index=True
1349 )
1350
1351 class Meta:
1352 abstract = True
1353 ordering = ['type__nom', '-date_fin']
1354
1355 def __unicode__(self):
1356 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
1357
1358
1359 class Remuneration_(RemunerationMixin, DevisableMixin):
1360 """
1361 Structure de rémunération (données budgétaires) en situation normale
1362 pour un Dossier. Si un Evenement existe, utiliser la structure de
1363 rémunération EvenementRemuneration de cet événement.
1364 """
1365 objects = RemunerationManager()
1366
1367 @staticmethod
1368 def find_yearly_range(from_date, to_date, year):
1369 today = date.today()
1370 year = year or date.today().year
1371 year_start = date(year, 1, 1)
1372 year_end = date(year, 12, 31)
1373
1374 def constrain_to_year(*dates):
1375 """
1376 S'assure que les dates soient dans le range year_start a
1377 year_end
1378 """
1379 return [min(max(year_start, d), year_end)
1380 for d in dates]
1381
1382 start_date = max(
1383 from_date or year_start, year_start)
1384 end_date = min(
1385 to_date or year_end, year_end)
1386
1387 start_date, end_date = constrain_to_year(start_date, end_date)
1388
1389 jours_annee = (year_end - year_start).days
1390 jours_dates = (end_date - start_date).days
1391 factor = Decimal(str(jours_dates)) / Decimal(str(jours_annee))
1392
1393 return start_date, end_date, factor
1394
1395
1396 def montant_ajuste_euros(self, annee=None):
1397 """
1398 Le montant ajusté représente le montant annuel, ajusté sur la
1399 période de temps travaillée, multipliée par le ratio de temps
1400 travaillé (en rapport au temps plein).
1401 """
1402 date_debut, date_fin, factor = self.find_yearly_range(
1403 self.date_debut,
1404 self.date_fin,
1405 annee,
1406 )
1407
1408 montant_euros = Decimal(str(self.montant_euros_float()) or '0')
1409
1410 if self.type.nature_remuneration != u'Accessoire':
1411 dossier = getattr(self, 'dossier', None)
1412 if not dossier:
1413 """
1414 Dans le cas d'un DossierComparaisonRemuneration, il
1415 n'y a plus de reference au dossier.
1416 """
1417 regime_travail = REGIME_TRAVAIL_DEFAULT
1418 else:
1419 regime_travail = self.dossier.regime_travail
1420 return (montant_euros * factor *
1421 regime_travail / 100)
1422 else:
1423 return montant_euros
1424
1425 def montant_mois(self):
1426 return round(self.montant / 12, 2)
1427
1428 def montant_avec_regime(self):
1429 return round(self.montant * (self.dossier.regime_travail / 100), 2)
1430
1431 def montant_euro_mois(self):
1432 return round(self.montant_euros() / 12, 2)
1433
1434 def __unicode__(self):
1435 try:
1436 devise = self.devise.code
1437 except:
1438 devise = "???"
1439 return "%s %s" % (self.montant, devise)
1440
1441 class Meta:
1442 abstract = True
1443 verbose_name = u"Rémunération"
1444 verbose_name_plural = u"Rémunérations"
1445
1446
1447 class Remuneration(Remuneration_):
1448 dossier = models.ForeignKey(
1449 Dossier, db_column='dossier', related_name='rh_remunerations'
1450 )
1451
1452 reversion.register(Remuneration, format='xml')
1453
1454
1455 ### CONTRATS
1456
1457 class Contrat_(models.Model):
1458 """
1459 Document juridique qui encadre la relation de travail d'un Employe
1460 pour un Poste particulier. Pour un Dossier (qui documente cette
1461 relation de travail) plusieurs contrats peuvent être associés.
1462 """
1463 objects = ContratManager()
1464 type_contrat = models.ForeignKey(
1465 'TypeContrat', db_column='type_contrat',
1466 verbose_name=u'type de contrat', related_name='+'
1467 )
1468 date_debut = models.DateField(
1469 u"date de début", db_index=True
1470 )
1471 date_fin = models.DateField(
1472 u"date de fin", null=True, blank=True, db_index=True
1473 )
1474 fichier = models.FileField(
1475 upload_to=contrat_dispatch, storage=storage_prive, null=True,
1476 blank=True, max_length=255
1477 )
1478
1479 class Meta:
1480 abstract = True
1481 ordering = ['dossier__employe__nom']
1482 verbose_name = u"Contrat"
1483 verbose_name_plural = u"Contrats"
1484
1485 def __unicode__(self):
1486 return u'%s - %s' % (self.dossier, self.id)
1487
1488
1489 class Contrat(Contrat_):
1490 dossier = models.ForeignKey(
1491 Dossier, db_column='dossier', related_name='rh_contrats'
1492 )
1493
1494 reversion.register(Contrat, format='xml')
1495
1496
1497 ### RÉFÉRENCES RH
1498
1499 class CategorieEmploi(models.Model):
1500 """
1501 Catégorie utilisée dans la gestion des Postes.
1502 Catégorie supérieure à TypePoste.
1503 """
1504 nom = models.CharField(max_length=255)
1505
1506 class Meta:
1507 ordering = ('nom',)
1508 verbose_name = u"catégorie d'emploi"
1509 verbose_name_plural = u"catégories d'emploi"
1510
1511 def __unicode__(self):
1512 return self.nom
1513
1514 reversion.register(CategorieEmploi, format='xml')
1515
1516
1517 class FamilleProfessionnelle(models.Model):
1518 """
1519 Famille professionnelle d'un poste.
1520 """
1521 nom = models.CharField(max_length=100)
1522
1523 class Meta:
1524 ordering = ('nom',)
1525 verbose_name = u'famille professionnelle'
1526 verbose_name_plural = u'familles professionnelles'
1527
1528 def __unicode__(self):
1529 return self.nom
1530
1531 reversion.register(FamilleProfessionnelle, format='xml')
1532
1533
1534 class TypePoste(Archivable):
1535 """
1536 Catégorie de Poste.
1537 """
1538 nom = models.CharField(max_length=255)
1539 nom_feminin = models.CharField(u"nom féminin", max_length=255)
1540 is_responsable = models.BooleanField(
1541 u"poste de responsabilité", default=False
1542 )
1543 categorie_emploi = models.ForeignKey(
1544 CategorieEmploi, db_column='categorie_emploi', related_name='+',
1545 verbose_name=u"catégorie d'emploi"
1546 )
1547 famille_professionnelle = models.ForeignKey(
1548 FamilleProfessionnelle, related_name='types_de_poste',
1549 verbose_name=u"famille professionnelle", blank=True, null=True
1550 )
1551
1552 class Meta:
1553 ordering = ['nom']
1554 verbose_name = u"Type de poste"
1555 verbose_name_plural = u"Types de poste"
1556
1557 def __unicode__(self):
1558 return u'%s' % (self.nom)
1559
1560 reversion.register(TypePoste, format='xml')
1561
1562
1563 TYPE_PAIEMENT_CHOICES = (
1564 (u'Régulier', u'Régulier'),
1565 (u'Ponctuel', u'Ponctuel'),
1566 )
1567
1568 NATURE_REMUNERATION_CHOICES = (
1569 (u'Traitement', u'Traitement'),
1570 (u'Indemnité', u'Indemnités autres'),
1571 (u'Charges', u'Charges patronales'),
1572 (u'Accessoire', u'Accessoires'),
1573 (u'RAS', u'Rémunération autre source'),
1574 )
1575
1576
1577 class TypeRemuneration(Archivable):
1578 """
1579 Catégorie de Remuneration.
1580 """
1581
1582 objects = models.Manager()
1583 sans_archives = ArchivableManager()
1584
1585 nom = models.CharField(max_length=255)
1586 type_paiement = models.CharField(
1587 u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
1588 )
1589
1590 nature_remuneration = models.CharField(
1591 u"nature de la rémunération", max_length=30,
1592 choices=NATURE_REMUNERATION_CHOICES
1593 )
1594
1595 class Meta:
1596 ordering = ['nom']
1597 verbose_name = u"Type de rémunération"
1598 verbose_name_plural = u"Types de rémunération"
1599
1600 def __unicode__(self):
1601 return self.nom
1602
1603 reversion.register(TypeRemuneration, format='xml')
1604
1605
1606 class TypeRevalorisation(Archivable):
1607 """
1608 Justification du changement de la Remuneration.
1609 (Actuellement utilisé dans aucun traitement informatique.)
1610 """
1611 nom = models.CharField(max_length=255)
1612
1613 class Meta:
1614 ordering = ['nom']
1615 verbose_name = u"Type de revalorisation"
1616 verbose_name_plural = u"Types de revalorisation"
1617
1618 def __unicode__(self):
1619 return u'%s' % (self.nom)
1620
1621 reversion.register(TypeRevalorisation, format='xml')
1622
1623
1624 class Service(Archivable):
1625 """
1626 Unité administrative où les Postes sont rattachés.
1627 """
1628 nom = models.CharField(max_length=255)
1629
1630 class Meta:
1631 ordering = ['nom']
1632 verbose_name = u"service"
1633 verbose_name_plural = u"services"
1634
1635 def __unicode__(self):
1636 return self.nom
1637
1638 reversion.register(Service, format='xml')
1639
1640
1641 TYPE_ORGANISME_CHOICES = (
1642 ('MAD', 'Mise à disposition'),
1643 ('DET', 'Détachement'),
1644 )
1645
1646
1647 class OrganismeBstg(models.Model):
1648 """
1649 Organisation d'où provient un Employe mis à disposition (MAD) de
1650 ou détaché (DET) à l'AUF à titre gratuit.
1651
1652 (BSTG = bien et service à titre gratuit.)
1653 """
1654 nom = models.CharField(max_length=255)
1655 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
1656 pays = models.ForeignKey(ref.Pays, to_field='code',
1657 db_column='pays',
1658 related_name='organismes_bstg',
1659 null=True, blank=True)
1660
1661 class Meta:
1662 ordering = ['type', 'nom']
1663 verbose_name = u"Organisme BSTG"
1664 verbose_name_plural = u"Organismes BSTG"
1665
1666 def __unicode__(self):
1667 return u'%s (%s)' % (self.nom, self.get_type_display())
1668
1669 reversion.register(OrganismeBstg, format='xml')
1670
1671
1672 class Statut(Archivable):
1673 """
1674 Statut de l'Employe dans le cadre d'un Dossier particulier.
1675 """
1676 # Identification
1677 code = models.CharField(
1678 max_length=25, unique=True,
1679 help_text=(
1680 u"Saisir un code court mais lisible pour ce statut : "
1681 u"le code est utilisé pour associer les statuts aux autres "
1682 u"données tout en demeurant plus lisible qu'un identifiant "
1683 u"numérique."
1684 )
1685 )
1686 nom = models.CharField(max_length=255)
1687
1688 class Meta:
1689 ordering = ['code']
1690 verbose_name = u"Statut d'employé"
1691 verbose_name_plural = u"Statuts d'employé"
1692
1693 def __unicode__(self):
1694 return u'%s : %s' % (self.code, self.nom)
1695
1696 reversion.register(Statut, format='xml')
1697
1698
1699 TYPE_CLASSEMENT_CHOICES = (
1700 ('S', 'S -Soutien'),
1701 ('T', 'T - Technicien'),
1702 ('P', 'P - Professionel'),
1703 ('C', 'C - Cadre'),
1704 ('D', 'D - Direction'),
1705 ('SO', 'SO - Sans objet [expatriés]'),
1706 ('HG', 'HG - Hors grille [direction]'),
1707 )
1708
1709
1710 class ClassementManager(models.Manager):
1711 """
1712 Ordonner les spcéfiquement les classements.
1713 """
1714 def get_query_set(self):
1715 qs = super(ClassementManager, self).get_query_set()
1716 qs = qs.extra(select={
1717 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1718 })
1719 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
1720 return qs.all()
1721
1722
1723 class ClassementArchivableManager(ClassementManager,
1724 ArchivableManager):
1725 pass
1726
1727
1728 class Classement_(Archivable):
1729 """
1730 Éléments de classement de la
1731 "Grille générique de classement hiérarchique".
1732
1733 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1734 classement dans la grille. Le classement donne le coefficient utilisé dans:
1735
1736 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1737 """
1738 objects = ClassementManager()
1739 sans_archives = ClassementArchivableManager()
1740
1741 # Identification
1742 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1743 echelon = models.IntegerField(u"échelon", blank=True, default=0)
1744 degre = models.IntegerField(u"degré", blank=True, default=0)
1745 coefficient = models.FloatField(u"coefficient", blank=True, null=True)
1746
1747 # Méta
1748 # annee # au lieu de date_debut et date_fin
1749 commentaire = models.TextField(null=True, blank=True)
1750
1751 class Meta:
1752 abstract = True
1753 ordering = ['type', 'echelon', 'degre', 'coefficient']
1754 verbose_name = u"Classement"
1755 verbose_name_plural = u"Classements"
1756
1757 def __unicode__(self):
1758 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
1759
1760
1761 class Classement(Classement_):
1762 __doc__ = Classement_.__doc__
1763
1764 reversion.register(Classement, format='xml')
1765
1766
1767 class TauxChange_(models.Model):
1768 """
1769 Taux de change de la devise vers l'euro (EUR)
1770 pour chaque année budgétaire.
1771 """
1772 # Identification
1773 devise = models.ForeignKey('Devise', db_column='devise')
1774 annee = models.IntegerField(u"année")
1775 taux = models.FloatField(u"taux vers l'euro")
1776
1777 class Meta:
1778 abstract = True
1779 ordering = ['-annee', 'devise__code']
1780 verbose_name = u"Taux de change"
1781 verbose_name_plural = u"Taux de change"
1782 unique_together = ('devise', 'annee')
1783
1784 def __unicode__(self):
1785 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1786
1787
1788 class TauxChange(TauxChange_):
1789 __doc__ = TauxChange_.__doc__
1790
1791 reversion.register(TauxChange, format='xml')
1792
1793
1794 class ValeurPointManager(models.Manager):
1795
1796 def get_query_set(self):
1797 return super(ValeurPointManager, self).get_query_set() \
1798 .select_related('devise', 'implantation')
1799
1800
1801 class ValeurPoint_(models.Model):
1802 """
1803 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1804 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1805 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1806
1807 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1808 """
1809
1810 objects = models.Manager()
1811 actuelles = ValeurPointManager()
1812
1813 valeur = models.FloatField(null=True)
1814 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
1815 implantation = models.ForeignKey(ref.Implantation,
1816 db_column='implantation',
1817 related_name='%(app_label)s_valeur_point')
1818 # Méta
1819 annee = models.IntegerField()
1820
1821 class Meta:
1822 ordering = ['-annee', 'implantation__nom']
1823 abstract = True
1824 verbose_name = u"Valeur du point"
1825 verbose_name_plural = u"Valeurs du point"
1826 unique_together = ('implantation', 'annee')
1827
1828 def __unicode__(self):
1829 return u'%s %s %s [%s] %s' % (
1830 self.devise.code, self.annee, self.valeur,
1831 self.implantation.nom_court, self.devise.nom
1832 )
1833
1834
1835 class ValeurPoint(ValeurPoint_):
1836 __doc__ = ValeurPoint_.__doc__
1837
1838 reversion.register(ValeurPoint, format='xml')
1839
1840
1841 class Devise(Archivable):
1842 """
1843 Devise monétaire.
1844 """
1845 code = models.CharField(max_length=10, unique=True)
1846 nom = models.CharField(max_length=255)
1847
1848 class Meta:
1849 ordering = ['code']
1850 verbose_name = u"devise"
1851 verbose_name_plural = u"devises"
1852
1853 def __unicode__(self):
1854 return u'%s - %s' % (self.code, self.nom)
1855
1856 reversion.register(Devise, format='xml')
1857
1858
1859 class TypeContrat(Archivable):
1860 """
1861 Type de contrat.
1862 """
1863 nom = models.CharField(max_length=255)
1864 nom_long = models.CharField(max_length=255)
1865
1866 class Meta:
1867 ordering = ['nom']
1868 verbose_name = u"Type de contrat"
1869 verbose_name_plural = u"Types de contrat"
1870
1871 def __unicode__(self):
1872 return u'%s' % (self.nom)
1873
1874 reversion.register(TypeContrat, format='xml')
1875
1876
1877 ### AUTRES
1878
1879 class ResponsableImplantationProxy(ref.Implantation):
1880
1881 def save(self):
1882 pass
1883
1884 class Meta:
1885 managed = False
1886 proxy = True
1887 verbose_name = u"Responsable d'implantation"
1888 verbose_name_plural = u"Responsables d'implantation"
1889
1890
1891 class ResponsableImplantation(models.Model):
1892 """
1893 Le responsable d'une implantation.
1894 Anciennement géré sur le Dossier du responsable.
1895 """
1896 employe = models.ForeignKey(
1897 'Employe', db_column='employe', related_name='+', null=True,
1898 blank=True
1899 )
1900 implantation = models.OneToOneField(
1901 "ResponsableImplantationProxy", db_column='implantation',
1902 related_name='responsable', unique=True
1903 )
1904
1905 def __unicode__(self):
1906 return u'%s : %s' % (self.implantation, self.employe)
1907
1908 class Meta:
1909 ordering = ['implantation__nom']
1910 verbose_name = "Responsable d'implantation"
1911 verbose_name_plural = "Responsables d'implantation"
1912
1913 reversion.register(ResponsableImplantation, format='xml')
1914
1915
1916 class UserProfile(models.Model):
1917 user = models.OneToOneField(User, related_name='profile')
1918 zones_administratives = models.ManyToManyField(
1919 ref.ZoneAdministrative,
1920 related_name='profiles'
1921 )
1922 class Meta:
1923 verbose_name = "Permissions sur zones administratives"
1924 verbose_name_plural = "Permissions sur zones administratives"
1925
1926 def __unicode__(self):
1927 return self.user.__unicode__()
1928
1929 reversion.register(UserProfile, format='xml')
1930
1931
1932
1933 TYPES_CHANGEMENT = (
1934 ('NO', 'Arrivée'),
1935 ('MO', 'Mobilité'),
1936 ('DE', 'Départ'),
1937 )
1938
1939
1940 class ChangementPersonnelNotifications(models.Model):
1941 class Meta:
1942 verbose_name = u"Destinataire pour notices de mouvement de personnel"
1943 verbose_name_plural = u"Destinataires pour notices de mouvement de personnel"
1944
1945 type = models.CharField(
1946 max_length=2,
1947 choices = TYPES_CHANGEMENT,
1948 unique=True,
1949 )
1950
1951 destinataires = models.ManyToManyField(
1952 ref.Employe,
1953 related_name='changement_notifications',
1954 )
1955
1956 def __unicode__(self):
1957 return '%s: %s' % (
1958 self.get_type_display(), ','.join(
1959 self.destinataires.all().values_list(
1960 'courriel', flat=True))
1961 )
1962
1963
1964 class ChangementPersonnel(models.Model):
1965 """
1966 Une notice qui enregistre un changement de personnel, incluant:
1967
1968 * Nouveaux employés
1969 * Mouvement de personnel
1970 * Départ d'employé
1971 """
1972
1973 class Meta:
1974 verbose_name = u"Mouvement de personnel"
1975 verbose_name_plural = u"Mouvements de personnel"
1976
1977 def __unicode__(self):
1978 return '%s: %s' % (self.dossier.__unicode__(),
1979 self.get_type_display())
1980
1981 @classmethod
1982 def create_changement(cls, dossier, type):
1983 # If this employe has existing Changement, set them to invalid.
1984 cls.objects.filter(dossier__employe=dossier.employe).update(valide=False)
1985
1986 # Create a new one.
1987 cls.objects.create(
1988 dossier=dossier,
1989 type=type,
1990 valide=True,
1991 communique=False,
1992 )
1993
1994
1995 @classmethod
1996 def post_save_handler(cls,
1997 sender,
1998 instance,
1999 created,
2000 using,
2001 **kw):
2002
2003 # This defines the time limit used when checking in previous
2004 # files to see if an employee if new. Basically, if emloyee
2005 # left his position new_file.date_debut -
2006 # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
2007 # if a new file is created for this employee, he will bec
2008 # onsidered "NEW" and a notice will be created to this effect.
2009 NEW_EMPLOYE_THRESHOLD = datetime.timedelta(7) # 7 days.
2010
2011 other_dossier_qs = instance.employe.rh_dossiers.exclude(
2012 id=instance.id)
2013 dd = instance.date_debut
2014 df = instance.date_fin
2015 today = date.today()
2016
2017 # Here, verify differences between the instance, before and
2018 # after the save.
2019 df_has_changed = False
2020
2021 if created:
2022 if df != None:
2023 df_has_changed = True
2024 else:
2025 df_has_changed = (df != instance.before_save.date_fin and
2026 df != None)
2027
2028
2029 # VERIFICATIONS:
2030
2031 # Date de fin est None et c'est une nouvelle instance de
2032 # Dossier
2033 if not df and created:
2034 # QS for finding other dossiers with a date_fin of None OR
2035 # with a date_fin >= to this dossier's date_debut
2036 exists_recent_file_qs = other_dossier_qs.filter(
2037 Q(date_fin__isnull=True) |
2038 Q(date_fin__gte=dd - NEW_EMPLOYE_THRESHOLD)
2039 )
2040
2041 # 1. If existe un Dossier récent
2042 if exists_recent_file_qs.count() > 0:
2043 cls.create_changement(
2044 instance,
2045 'MO',
2046 )
2047 # 2. Il n'existe un Dossier récent, et c'est une nouvelle
2048 # instance de Dossier:
2049 else:
2050 cls.create_changement(
2051 instance,
2052 'NO',
2053 )
2054
2055 elif not df and not created and cls.objects.filter(
2056 valide=True,
2057 date_creation__gte=today - NEW_EMPLOYE_THRESHOLD,
2058 type='DE',
2059 ).count() > 0:
2060 cls.create_changement(
2061 instance,
2062 'MO',
2063 )
2064
2065 # Date de fin a été modifiée:
2066 if df_has_changed:
2067 # QS for other active files (date_fin == None), excludes
2068 # instance.
2069 exists_active_files_qs = other_dossier_qs.filter(
2070 Q(date_fin__isnull=True))
2071
2072 # 3. Date de fin a été modifiée et il n'existe aucun autre
2073 # dossier actifs: Depart
2074 if exists_active_files_qs.count() == 0:
2075 cls.create_changement(
2076 instance,
2077 'DE',
2078 )
2079 # 4. Dossier a une nouvelle date de fin par contre
2080 # d'autres dossiers actifs existent déjà: Mouvement
2081 else:
2082 cls.create_changement(
2083 instance,
2084 'MO',
2085 )
2086
2087
2088 dossier = models.ForeignKey(
2089 Dossier,
2090 related_name='mouvements',
2091 )
2092
2093 valide = models.BooleanField(default=True)
2094 date_creation = models.DateTimeField(
2095 auto_now_add=True)
2096 communique = models.BooleanField(
2097 u'Communiqué',
2098 default=False,
2099 )
2100 date_communication = models.DateTimeField(
2101 null=True,
2102 blank=True,
2103 )
2104
2105 type = models.CharField(
2106 max_length=2,
2107 choices = TYPES_CHANGEMENT,
2108 )
2109
2110 reversion.register(ChangementPersonnel, format='xml')
2111
2112
2113 def dossier_pre_save_handler(sender,
2114 instance,
2115 using,
2116 **kw):
2117 # Store a copy of the model before save is called.
2118 if instance.pk is not None:
2119 instance.before_save = Dossier.objects.get(pk=instance.pk)
2120 else:
2121 instance.before_save = None
2122
2123
2124 # Connect a pre_save handler that assigns a copy of the model as an
2125 # attribute in order to compare it in post_save.
2126 pre_save.connect(dossier_pre_save_handler, sender=Dossier)
2127
2128 post_save.connect(ChangementPersonnel.post_save_handler, sender=Dossier)
2129 post_save.connect(RHDossierClassementRecord.post_save_handler, sender=Dossier)
2130
2131