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