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