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