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