Done with historique
[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 câdre?",
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 # Attempt to pull previous classement from history.
1118 previous_classement = None
1119 has_changed = False
1120
1121 if not created:
1122 has_changed = (
1123 instance.classement.id !=
1124 instance.before_save.classement.id
1125 )
1126 try:
1127 previous_record = cls.objects.get(
1128 dossier=instance,
1129 classement=instance.before_save.classement,
1130 date_fin=None,
1131 )
1132 except cls.DoesNotExist:
1133 previous_record = None
1134 else:
1135 previous_classement = previous_record.classement
1136
1137
1138 if created or (not created and not previous_classement):
1139 cls.objects.create(
1140 date_debut=instance.date_debut,
1141 classement=instance.classement,
1142 dossier=instance,
1143 )
1144 elif has_changed and previous_classement:
1145 change_date = datetime.date.today()
1146 previous_record.date_fin = change_date
1147 previous_record.save()
1148 cls.objects.create(
1149 date_debut=change_date,
1150 classement=instance.classement,
1151 dossier=instance,
1152 )
1153
1154 # elif not created:
1155 # cmp_inst = self.before_save
1156
1157 # # Classement has changed!
1158 # if (cmp_inst.classement.id !=
1159 # instance.classement.id):
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169 class DossierPiece_(models.Model):
1170 """
1171 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
1172 Ex.: Lettre de motivation.
1173 """
1174 nom = models.CharField(max_length=255)
1175 fichier = models.FileField(
1176 upload_to=dossier_piece_dispatch, storage=storage_prive
1177 )
1178
1179 class Meta:
1180 abstract = True
1181 ordering = ['nom']
1182
1183 def __unicode__(self):
1184 return u'%s' % (self.nom)
1185
1186
1187 class DossierPiece(DossierPiece_):
1188 dossier = models.ForeignKey(
1189 Dossier, db_column='dossier', related_name='rh_dossierpieces'
1190 )
1191
1192 reversion.register(DossierPiece, format='xml')
1193
1194 class DossierCommentaire(Commentaire):
1195 dossier = models.ForeignKey(
1196 Dossier, db_column='dossier', related_name='commentaires'
1197 )
1198
1199 reversion.register(DossierCommentaire, format='xml')
1200
1201
1202 class DossierComparaison_(models.Model, DevisableMixin):
1203 """
1204 Photo d'une comparaison salariale au moment de l'embauche.
1205 """
1206 objects = DossierComparaisonManager()
1207
1208 implantation = models.ForeignKey(
1209 ref.Implantation, related_name="+", null=True, blank=True
1210 )
1211 poste = models.CharField(max_length=255, null=True, blank=True)
1212 personne = models.CharField(max_length=255, null=True, blank=True)
1213 montant = models.IntegerField(null=True)
1214 devise = models.ForeignKey(
1215 'Devise', related_name='+', null=True, blank=True
1216 )
1217
1218 class Meta:
1219 abstract = True
1220
1221 def __unicode__(self):
1222 return "%s (%s)" % (self.poste, self.personne)
1223
1224
1225 class DossierComparaison(DossierComparaison_):
1226 dossier = models.ForeignKey(
1227 Dossier, related_name='rh_comparaisons'
1228 )
1229
1230 reversion.register(DossierComparaison, format='xml')
1231
1232
1233 ### RÉMUNÉRATION
1234
1235 class RemunerationMixin(models.Model):
1236
1237 # Identification
1238 type = models.ForeignKey(
1239 'TypeRemuneration', db_column='type', related_name='+',
1240 verbose_name=u"type de rémunération"
1241 )
1242 type_revalorisation = models.ForeignKey(
1243 'TypeRevalorisation', db_column='type_revalorisation',
1244 related_name='+', verbose_name=u"type de revalorisation",
1245 null=True, blank=True
1246 )
1247 montant = models.DecimalField(
1248 null=True, blank=True, max_digits=12, decimal_places=2
1249 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1250 devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
1251
1252 # commentaire = precision
1253 commentaire = models.CharField(max_length=255, null=True, blank=True)
1254
1255 # date_debut = anciennement date_effectif
1256 date_debut = models.DateField(
1257 u"date de début", null=True, blank=True, db_index=True
1258 )
1259 date_fin = models.DateField(
1260 u"date de fin", null=True, blank=True, db_index=True
1261 )
1262
1263 class Meta:
1264 abstract = True
1265 ordering = ['type__nom', '-date_fin']
1266
1267 def __unicode__(self):
1268 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
1269
1270
1271 class Remuneration_(RemunerationMixin, DevisableMixin):
1272 """
1273 Structure de rémunération (données budgétaires) en situation normale
1274 pour un Dossier. Si un Evenement existe, utiliser la structure de
1275 rémunération EvenementRemuneration de cet événement.
1276 """
1277 objects = RemunerationManager()
1278
1279 def montant_mois(self):
1280 return round(self.montant / 12, 2)
1281
1282 def montant_avec_regime(self):
1283 return round(self.montant * (self.dossier.regime_travail / 100), 2)
1284
1285 def montant_euro_mois(self):
1286 return round(self.montant_euros() / 12, 2)
1287
1288 def __unicode__(self):
1289 try:
1290 devise = self.devise.code
1291 except:
1292 devise = "???"
1293 return "%s %s" % (self.montant, devise)
1294
1295 class Meta:
1296 abstract = True
1297 verbose_name = u"Rémunération"
1298 verbose_name_plural = u"Rémunérations"
1299
1300
1301 class Remuneration(Remuneration_):
1302 dossier = models.ForeignKey(
1303 Dossier, db_column='dossier', related_name='rh_remunerations'
1304 )
1305
1306 reversion.register(Remuneration, format='xml')
1307
1308
1309 ### CONTRATS
1310
1311 class Contrat_(models.Model):
1312 """
1313 Document juridique qui encadre la relation de travail d'un Employe
1314 pour un Poste particulier. Pour un Dossier (qui documente cette
1315 relation de travail) plusieurs contrats peuvent être associés.
1316 """
1317 objects = ContratManager()
1318 type_contrat = models.ForeignKey(
1319 'TypeContrat', db_column='type_contrat',
1320 verbose_name=u'type de contrat', related_name='+'
1321 )
1322 date_debut = models.DateField(
1323 u"date de début", db_index=True
1324 )
1325 date_fin = models.DateField(
1326 u"date de fin", null=True, blank=True, db_index=True
1327 )
1328 fichier = models.FileField(
1329 upload_to=contrat_dispatch, storage=storage_prive, null=True,
1330 blank=True
1331 )
1332
1333 class Meta:
1334 abstract = True
1335 ordering = ['dossier__employe__nom']
1336 verbose_name = u"Contrat"
1337 verbose_name_plural = u"Contrats"
1338
1339 def __unicode__(self):
1340 return u'%s - %s' % (self.dossier, self.id)
1341
1342
1343 class Contrat(Contrat_):
1344 dossier = models.ForeignKey(
1345 Dossier, db_column='dossier', related_name='rh_contrats'
1346 )
1347
1348 reversion.register(Contrat, format='xml')
1349
1350
1351 ### RÉFÉRENCES RH
1352
1353 class CategorieEmploi(models.Model):
1354 """
1355 Catégorie utilisée dans la gestion des Postes.
1356 Catégorie supérieure à TypePoste.
1357 """
1358 nom = models.CharField(max_length=255)
1359
1360 class Meta:
1361 ordering = ('nom',)
1362 verbose_name = u"catégorie d'emploi"
1363 verbose_name_plural = u"catégories d'emploi"
1364
1365 def __unicode__(self):
1366 return self.nom
1367
1368 reversion.register(CategorieEmploi, format='xml')
1369
1370
1371 class FamilleProfessionnelle(models.Model):
1372 """
1373 Famille professionnelle d'un poste.
1374 """
1375 nom = models.CharField(max_length=100)
1376
1377 class Meta:
1378 ordering = ('nom',)
1379 verbose_name = u'famille professionnelle'
1380 verbose_name_plural = u'familles professionnelles'
1381
1382 def __unicode__(self):
1383 return self.nom
1384
1385 reversion.register(FamilleProfessionnelle, format='xml')
1386
1387
1388 class TypePoste(Archivable):
1389 """
1390 Catégorie de Poste.
1391 """
1392 nom = models.CharField(max_length=255)
1393 nom_feminin = models.CharField(u"nom féminin", max_length=255)
1394 is_responsable = models.BooleanField(
1395 u"poste de responsabilité", default=False
1396 )
1397 categorie_emploi = models.ForeignKey(
1398 CategorieEmploi, db_column='categorie_emploi', related_name='+',
1399 verbose_name=u"catégorie d'emploi"
1400 )
1401 famille_professionnelle = models.ForeignKey(
1402 FamilleProfessionnelle, related_name='types_de_poste',
1403 verbose_name=u"famille professionnelle", blank=True, null=True
1404 )
1405
1406 class Meta:
1407 ordering = ['nom']
1408 verbose_name = u"Type de poste"
1409 verbose_name_plural = u"Types de poste"
1410
1411 def __unicode__(self):
1412 return u'%s' % (self.nom)
1413
1414 reversion.register(TypePoste, format='xml')
1415
1416
1417 TYPE_PAIEMENT_CHOICES = (
1418 (u'Régulier', u'Régulier'),
1419 (u'Ponctuel', u'Ponctuel'),
1420 )
1421
1422 NATURE_REMUNERATION_CHOICES = (
1423 (u'Accessoire', u'Traitement ponctuel'),
1424 (u'Charges', u'Charges patronales'),
1425 (u'Indemnité', u'Indemnité'),
1426 (u'RAS', u'Rémunération autre source'),
1427 (u'Traitement', u'Traitement'),
1428 )
1429
1430
1431 class TypeRemuneration(Archivable):
1432 """
1433 Catégorie de Remuneration.
1434 """
1435
1436 nom = models.CharField(max_length=255)
1437 type_paiement = models.CharField(
1438 u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
1439 )
1440
1441 nature_remuneration = models.CharField(
1442 u"nature de la rémunération", max_length=30,
1443 choices=NATURE_REMUNERATION_CHOICES
1444 )
1445
1446 class Meta:
1447 ordering = ['nom']
1448 verbose_name = u"Type de rémunération"
1449 verbose_name_plural = u"Types de rémunération"
1450
1451 def __unicode__(self):
1452 return self.nom
1453
1454 reversion.register(TypeRemuneration, format='xml')
1455
1456
1457 class TypeRevalorisation(Archivable):
1458 """
1459 Justification du changement de la Remuneration.
1460 (Actuellement utilisé dans aucun traitement informatique.)
1461 """
1462 nom = models.CharField(max_length=255)
1463
1464 class Meta:
1465 ordering = ['nom']
1466 verbose_name = u"Type de revalorisation"
1467 verbose_name_plural = u"Types de revalorisation"
1468
1469 def __unicode__(self):
1470 return u'%s' % (self.nom)
1471
1472 reversion.register(TypeRevalorisation, format='xml')
1473
1474
1475 class Service(Archivable):
1476 """
1477 Unité administrative où les Postes sont rattachés.
1478 """
1479 nom = models.CharField(max_length=255)
1480
1481 class Meta:
1482 ordering = ['nom']
1483 verbose_name = u"service"
1484 verbose_name_plural = u"services"
1485
1486 def __unicode__(self):
1487 return self.nom
1488
1489 reversion.register(Service, format='xml')
1490
1491
1492 TYPE_ORGANISME_CHOICES = (
1493 ('MAD', 'Mise à disposition'),
1494 ('DET', 'Détachement'),
1495 )
1496
1497
1498 class OrganismeBstg(models.Model):
1499 """
1500 Organisation d'où provient un Employe mis à disposition (MAD) de
1501 ou détaché (DET) à l'AUF à titre gratuit.
1502
1503 (BSTG = bien et service à titre gratuit.)
1504 """
1505 nom = models.CharField(max_length=255)
1506 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
1507 pays = models.ForeignKey(ref.Pays, to_field='code',
1508 db_column='pays',
1509 related_name='organismes_bstg',
1510 null=True, blank=True)
1511
1512 class Meta:
1513 ordering = ['type', 'nom']
1514 verbose_name = u"Organisme BSTG"
1515 verbose_name_plural = u"Organismes BSTG"
1516
1517 def __unicode__(self):
1518 return u'%s (%s)' % (self.nom, self.get_type_display())
1519
1520 reversion.register(OrganismeBstg, format='xml')
1521
1522
1523 class Statut(Archivable):
1524 """
1525 Statut de l'Employe dans le cadre d'un Dossier particulier.
1526 """
1527 # Identification
1528 code = models.CharField(
1529 max_length=25, unique=True,
1530 help_text=(
1531 u"Saisir un code court mais lisible pour ce statut : "
1532 u"le code est utilisé pour associer les statuts aux autres "
1533 u"données tout en demeurant plus lisible qu'un identifiant "
1534 u"numérique."
1535 )
1536 )
1537 nom = models.CharField(max_length=255)
1538
1539 class Meta:
1540 ordering = ['code']
1541 verbose_name = u"Statut d'employé"
1542 verbose_name_plural = u"Statuts d'employé"
1543
1544 def __unicode__(self):
1545 return u'%s : %s' % (self.code, self.nom)
1546
1547 reversion.register(Statut, format='xml')
1548
1549
1550 TYPE_CLASSEMENT_CHOICES = (
1551 ('S', 'S -Soutien'),
1552 ('T', 'T - Technicien'),
1553 ('P', 'P - Professionel'),
1554 ('C', 'C - Cadre'),
1555 ('D', 'D - Direction'),
1556 ('SO', 'SO - Sans objet [expatriés]'),
1557 ('HG', 'HG - Hors grille [direction]'),
1558 )
1559
1560
1561 class ClassementManager(ArchivableManager):
1562 """
1563 Ordonner les spcéfiquement les classements.
1564 """
1565 def get_query_set(self):
1566 qs = super(self.__class__, self).get_query_set()
1567 qs = qs.extra(select={
1568 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1569 })
1570 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
1571 return qs.all()
1572
1573
1574 class Classement_(Archivable):
1575 """
1576 Éléments de classement de la
1577 "Grille générique de classement hiérarchique".
1578
1579 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1580 classement dans la grille. Le classement donne le coefficient utilisé dans:
1581
1582 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1583 """
1584 objects = ClassementManager()
1585
1586 # Identification
1587 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1588 echelon = models.IntegerField(u"échelon", blank=True, default=0)
1589 degre = models.IntegerField(u"degré", blank=True, default=0)
1590 coefficient = models.FloatField(u"coefficient", blank=True, null=True)
1591
1592 # Méta
1593 # annee # au lieu de date_debut et date_fin
1594 commentaire = models.TextField(null=True, blank=True)
1595
1596 class Meta:
1597 abstract = True
1598 ordering = ['type', 'echelon', 'degre', 'coefficient']
1599 verbose_name = u"Classement"
1600 verbose_name_plural = u"Classements"
1601
1602 def __unicode__(self):
1603 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
1604
1605
1606 class Classement(Classement_):
1607 __doc__ = Classement_.__doc__
1608
1609 reversion.register(Classement, format='xml')
1610
1611
1612 class TauxChange_(models.Model):
1613 """
1614 Taux de change de la devise vers l'euro (EUR)
1615 pour chaque année budgétaire.
1616 """
1617 # Identification
1618 devise = models.ForeignKey('Devise', db_column='devise')
1619 annee = models.IntegerField(u"année")
1620 taux = models.FloatField(u"taux vers l'euro")
1621
1622 class Meta:
1623 abstract = True
1624 ordering = ['-annee', 'devise__code']
1625 verbose_name = u"Taux de change"
1626 verbose_name_plural = u"Taux de change"
1627 unique_together = ('devise', 'annee')
1628
1629 def __unicode__(self):
1630 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1631
1632
1633 class TauxChange(TauxChange_):
1634 __doc__ = TauxChange_.__doc__
1635
1636 reversion.register(TauxChange, format='xml')
1637
1638
1639 class ValeurPointManager(models.Manager):
1640
1641 def get_query_set(self):
1642 return super(ValeurPointManager, self).get_query_set() \
1643 .select_related('devise', 'implantation')
1644
1645
1646 class ValeurPoint_(models.Model):
1647 """
1648 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1649 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1650 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1651
1652 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1653 """
1654
1655 objects = models.Manager()
1656 actuelles = ValeurPointManager()
1657
1658 valeur = models.FloatField(null=True)
1659 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
1660 implantation = models.ForeignKey(ref.Implantation,
1661 db_column='implantation',
1662 related_name='%(app_label)s_valeur_point')
1663 # Méta
1664 annee = models.IntegerField()
1665
1666 class Meta:
1667 ordering = ['-annee', 'implantation__nom']
1668 abstract = True
1669 verbose_name = u"Valeur du point"
1670 verbose_name_plural = u"Valeurs du point"
1671 unique_together = ('implantation', 'annee')
1672
1673 def __unicode__(self):
1674 return u'%s %s %s [%s] %s' % (
1675 self.devise.code, self.annee, self.valeur,
1676 self.implantation.nom_court, self.devise.nom
1677 )
1678
1679
1680 class ValeurPoint(ValeurPoint_):
1681 __doc__ = ValeurPoint_.__doc__
1682
1683 reversion.register(ValeurPoint, format='xml')
1684
1685
1686 class Devise(Archivable):
1687 """
1688 Devise monétaire.
1689 """
1690 code = models.CharField(max_length=10, unique=True)
1691 nom = models.CharField(max_length=255)
1692
1693 class Meta:
1694 ordering = ['code']
1695 verbose_name = u"devise"
1696 verbose_name_plural = u"devises"
1697
1698 def __unicode__(self):
1699 return u'%s - %s' % (self.code, self.nom)
1700
1701 reversion.register(Devise, format='xml')
1702
1703
1704 class TypeContrat(Archivable):
1705 """
1706 Type de contrat.
1707 """
1708 nom = models.CharField(max_length=255)
1709 nom_long = models.CharField(max_length=255)
1710
1711 class Meta:
1712 ordering = ['nom']
1713 verbose_name = u"Type de contrat"
1714 verbose_name_plural = u"Types de contrat"
1715
1716 def __unicode__(self):
1717 return u'%s' % (self.nom)
1718
1719 reversion.register(TypeContrat, format='xml')
1720
1721
1722 ### AUTRES
1723
1724 class ResponsableImplantationProxy(ref.Implantation):
1725
1726 def save(self):
1727 pass
1728
1729 class Meta:
1730 managed = False
1731 proxy = True
1732 verbose_name = u"Responsable d'implantation"
1733 verbose_name_plural = u"Responsables d'implantation"
1734
1735
1736 class ResponsableImplantation(models.Model):
1737 """
1738 Le responsable d'une implantation.
1739 Anciennement géré sur le Dossier du responsable.
1740 """
1741 employe = models.ForeignKey(
1742 'Employe', db_column='employe', related_name='+', null=True,
1743 blank=True
1744 )
1745 implantation = models.OneToOneField(
1746 "ResponsableImplantationProxy", db_column='implantation',
1747 related_name='responsable', unique=True
1748 )
1749
1750 def __unicode__(self):
1751 return u'%s : %s' % (self.implantation, self.employe)
1752
1753 class Meta:
1754 ordering = ['implantation__nom']
1755 verbose_name = "Responsable d'implantation"
1756 verbose_name_plural = "Responsables d'implantation"
1757
1758 reversion.register(ResponsableImplantation, format='xml')
1759
1760
1761 class UserProfile(models.Model):
1762 user = models.OneToOneField(User, related_name='profile')
1763 zones_administratives = models.ManyToManyField(
1764 ref.ZoneAdministrative,
1765 related_name='profiles'
1766 )
1767 class Meta:
1768 verbose_name = "Permissions sur zones administratives"
1769 verbose_name_plural = "Permissions sur zones administratives"
1770
1771 def __unicode__(self):
1772 return self.user.__unicode__()
1773
1774 reversion.register(UserProfile, format='xml')
1775
1776
1777
1778 TYPES_CHANGEMENT = (
1779 ('NO', 'Nouveau personnel'),
1780 ('MO', 'Mouvement de personnel'),
1781 ('DE', 'Départ de personnel'),
1782 )
1783
1784
1785 class ChangementPersonnelNotifications(models.Model):
1786 class Meta:
1787 verbose_name = u"Destinataire pour notices de changement de personnel"
1788 verbose_name_plural = u"Destinataires pour notices de changement de personnel"
1789
1790 type = models.CharField(
1791 max_length=2,
1792 choices = TYPES_CHANGEMENT,
1793 unique=True,
1794 )
1795
1796 destinataires = models.ManyToManyField(
1797 ref.Employe,
1798 related_name='changement_notifications',
1799 )
1800
1801 def __unicode__(self):
1802 return '%s: %s' % (
1803 self.get_type_display(), ','.join(
1804 self.destinataires.all().values_list(
1805 'courriel', flat=True))
1806 )
1807
1808
1809 class ChangementPersonnel(models.Model):
1810 """
1811 Une notice qui enregistre un changement de personnel, incluant:
1812
1813 * Nouveaux employés
1814 * Mouvement de personnel
1815 * Départ d'employé
1816 """
1817
1818 class Meta:
1819 verbose_name = u"Notification de changement du personnel"
1820 verbose_name_plural = u"Notifications de changement du personnel"
1821
1822 def __unicode__(self):
1823 return '%s: %s' % (self.dossier.__unicode__(),
1824 self.get_type_display())
1825
1826 @classmethod
1827 def create_changement(cls, dossier, type):
1828 # If this employe has existing Changement, set them to invalid.
1829 cls.objects.filter(dossier__employe=dossier.employe).update(valide=False)
1830
1831 # Create a new one.
1832 cls.objects.create(
1833 dossier=dossier,
1834 type=type,
1835 valide=True,
1836 communique=False,
1837 )
1838
1839
1840 @classmethod
1841 def post_save_handler(cls,
1842 sender,
1843 instance,
1844 created,
1845 using,
1846 **kw):
1847
1848 # This defines the time limit used when checking in previous
1849 # files to see if an employee if new. Basically, if emloyee
1850 # left his position new_file.date_debut -
1851 # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
1852 # if a new file is created for this employee, he will bec
1853 # onsidered "NEW" and a notice will be created to this effect.
1854 NEW_EMPLOYE_THRESHOLD = datetime.timedelta(7) # 7 days.
1855
1856 other_dossier_qs = instance.employe.rh_dossiers.exclude(
1857 id=instance.id)
1858 dd = instance.date_debut
1859 df = instance.date_fin
1860
1861 # Here, verify differences between the instance, before and
1862 # after the save.
1863 if created:
1864 if instance.before_save.date_fin != None:
1865 df_has_changed = True
1866 else:
1867 df_has_changed = (df != instance.before_save.date_fin and
1868 df != None)
1869
1870
1871 # VERIFICATIONS:
1872
1873 # Date de fin est None et c'est une nouvelle instance de
1874 # Dossier
1875 if not df and created:
1876 # QS for finding other dossiers with a date_fin of None OR
1877 # with a date_fin >= to this dossier's date_debut
1878 exists_recent_file_qs = other_dossier_qs.filter(
1879 Q(date_fin__isnull=True) |
1880 Q(date_fin__gte=dd - NEW_EMPLOYE_THRESHOLD)
1881 )
1882
1883 # 1. If existe un Dossier récent, et c'est une nouvelle
1884 # instance de Dossier:
1885 if exists_recent_file_qs.count() > 0:
1886 cls.create_changement(
1887 instance,
1888 'MO',
1889 )
1890 # 2. Il n'existe un Dossier récent, et c'est une nouvelle
1891 # instance de Dossier:
1892 else:
1893 cls.create_changement(
1894 instance,
1895 'NO',
1896 )
1897
1898
1899 # Date de fin a été modifiée:
1900 if df_has_changed:
1901 # QS for other active files (date_fin == None), excludes
1902 # instance.
1903 exists_active_files_qs = other_dossier_qs.filter(
1904 Q(date_fin__isnull=True))
1905
1906 # 3. Date de fin a été modifiée et il n'existe aucun autre
1907 # dossier actifs: Depart
1908 if exists_active_files_qs.count() == 0:
1909 cls.create_changement(
1910 instance,
1911 'DE',
1912 )
1913 # 4. Dossier a une nouvelle date de fin par contre
1914 # d'autres dossiers actifs existent déjà: Mouvement
1915 else:
1916 cls.create_changement(
1917 instance,
1918 'MO',
1919 )
1920
1921
1922 dossier = models.ForeignKey(
1923 Dossier,
1924 related_name='mouvements',
1925 )
1926
1927 valide = models.BooleanField(default=True)
1928 communique = models.BooleanField(default=False)
1929 date_communication = models.DateTimeField(
1930 null=True,
1931 blank=True,
1932 )
1933
1934 type = models.CharField(
1935 max_length=2,
1936 choices = TYPES_CHANGEMENT,
1937 )
1938
1939 reversion.register(ChangementPersonnel, format='xml')
1940
1941
1942 def dossier_pre_save_handler(sender,
1943 instance,
1944 using,
1945 **kw):
1946 # Store a copy of the model before save is called.
1947 if instance.pk is not None:
1948 instance.before_save = Dossier.objects.get(pk=instance.pk)
1949 else:
1950 instance.before_save = None
1951
1952
1953 # Connect a pre_save handler that assigns a copy of the model as an
1954 # attribute in order to compare it in post_save.
1955 pre_save.connect(dossier_pre_save_handler, sender=Dossier)
1956
1957 post_save.connect(ChangementPersonnel.post_save_handler, sender=Dossier)
1958 post_save.connect(RHDossierClassementRecord.post_save_handler, sender=Dossier)
1959
1960