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