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