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