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