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