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