Merge branch 'test' into dev
[auf_rh_dae.git] / project / rh / models.py
1 # -=- encoding: utf-8 -=-
2
3 from datetime import date
4 from decimal import Decimal
5
6 from django.core.files.storage import FileSystemStorage
7 from django.db import models
8 from django.conf import settings
9
10 from auf.django.metadata.models import AUFMetadata
11 from auf.django.metadata.managers import NoDeleteManager
12 import auf.django.references.models as ref
13 from validators import validate_date_passee
14 from dae.managers import SecurityManager
15
16 # Constantes
17 REGIME_TRAVAIL_DEFAULT = Decimal('100.00')
18 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = Decimal('35.00')
19
20
21 # Upload de fichiers
22 storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
23 base_url=settings.PRIVE_MEDIA_URL)
24
25 def poste_piece_dispatch(instance, filename):
26 path = "poste/%s/%s" % (instance.poste_id, filename)
27 return path
28
29 def dossier_piece_dispatch(instance, filename):
30 path = "dossier/%s/%s" % (instance.dossier_id, filename)
31 return path
32
33 def employe_piece_dispatch(instance, filename):
34 path = "employe/%s/%s" % (instance.employe_id, filename)
35 return path
36
37
38 class Commentaire(AUFMetadata):
39 texte = models.TextField()
40 owner = models.ForeignKey('auth.User', db_column='owner', related_name='+')
41
42 class Meta:
43 abstract = True
44 ordering = ['-date_creation']
45
46 def __unicode__(self):
47 return u'%s' % (self.texte)
48
49
50 ### POSTE
51
52 POSTE_APPEL_CHOICES = (
53 ('interne', 'Interne'),
54 ('externe', 'Externe'),
55 )
56
57 class PosteManager(SecurityManager):
58 """
59 Chargement de tous les objets FK existants sur chaque QuerySet.
60 """
61 prefixe_implantation = "implantation__region"
62
63 def get_query_set(self):
64 fkeys = (
65 'implantation',
66 'type_poste',
67 )
68 return super(PosteManager, self).get_query_set().select_related(*fkeys).all()
69
70 class Poste_(AUFMetadata):
71 """Un Poste est un emploi (job) à combler dans une implantation.
72 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
73 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
74 """
75
76 objects = PosteManager()
77
78 # Identification
79 nom = models.CharField(max_length=255,
80 verbose_name = u"Titre du poste", )
81 nom_feminin = models.CharField(max_length=255,
82 verbose_name = u"Titre du poste (au féminin)",
83 null=True)
84 implantation = models.ForeignKey(ref.Implantation,
85 db_column='implantation', related_name='+')
86 type_poste = models.ForeignKey('TypePoste', db_column='type_poste',
87 related_name='+',
88 null=True)
89 service = models.ForeignKey('Service', db_column='service',
90 related_name='+',
91 verbose_name = u"Direction/Service/Pôle support",
92 default=1) # default = Rectorat
93 responsable = models.ForeignKey('Poste', db_column='responsable',
94 related_name='+',
95 verbose_name = u"Poste du responsable",
96 default=149) # default = Recteur
97
98 # Contrat
99 regime_travail = models.DecimalField(max_digits=12, decimal_places=2,
100 default=REGIME_TRAVAIL_DEFAULT, null=True,
101 verbose_name = u"Temps de travail",
102 help_text="% du temps complet")
103 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
104 decimal_places=2, null=True,
105 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
106 verbose_name = u"Nb. heures par semaine")
107
108 # Recrutement
109 local = models.NullBooleanField(verbose_name = u"Local", default=True,
110 null=True, blank=True)
111 expatrie = models.NullBooleanField(verbose_name = u"Expatrié", default=False,
112 null=True, blank=True)
113 mise_a_disposition = models.NullBooleanField(
114 verbose_name = u"Mise à disposition",
115 null=True, default=False)
116 appel = models.CharField(max_length=10, null=True,
117 verbose_name = u"Appel à candidature",
118 choices=POSTE_APPEL_CHOICES,
119 default='interne')
120
121 # Rémunération
122 classement_min = models.ForeignKey('Classement',
123 db_column='classement_min', related_name='+',
124 null=True, blank=True)
125 classement_max = models.ForeignKey('Classement',
126 db_column='classement_max', related_name='+',
127 null=True, blank=True)
128 valeur_point_min = models.ForeignKey('ValeurPoint',
129 db_column='valeur_point_min', related_name='+',
130 null=True, blank=True)
131 valeur_point_max = models.ForeignKey('ValeurPoint',
132 db_column='valeur_point_max', related_name='+',
133 null=True, blank=True)
134 devise_min = models.ForeignKey('Devise', db_column='devise_min', null=True,
135 related_name='+', default=5)
136 devise_max = models.ForeignKey('Devise', db_column='devise_max', null=True,
137 related_name='+', default=5)
138 salaire_min = models.DecimalField(max_digits=12, decimal_places=2,
139 null=True, default=0)
140 salaire_max = models.DecimalField(max_digits=12, decimal_places=2,
141 null=True, default=0)
142 indemn_min = models.DecimalField(max_digits=12, decimal_places=2,
143 null=True, default=0)
144 indemn_max = models.DecimalField(max_digits=12, decimal_places=2,
145 null=True, default=0)
146 autre_min = models.DecimalField(max_digits=12, decimal_places=2,
147 null=True, default=0)
148 autre_max = models.DecimalField(max_digits=12, decimal_places=2,
149 null=True, default=0)
150
151 # Comparatifs de rémunération
152 devise_comparaison = models.ForeignKey('Devise', null=True,
153 db_column='devise_comparaison',
154 related_name='+',
155 default=5)
156 comp_locale_min = models.DecimalField(max_digits=12, decimal_places=2,
157 null=True, blank=True)
158 comp_locale_max = models.DecimalField(max_digits=12, decimal_places=2,
159 null=True, blank=True)
160 comp_universite_min = models.DecimalField(max_digits=12, decimal_places=2,
161 null=True, blank=True)
162 comp_universite_max = models.DecimalField(max_digits=12, decimal_places=2,
163 null=True, blank=True)
164 comp_fonctionpub_min = models.DecimalField(max_digits=12, decimal_places=2,
165 null=True, blank=True)
166 comp_fonctionpub_max = models.DecimalField(max_digits=12, decimal_places=2,
167 null=True, blank=True)
168 comp_ong_min = models.DecimalField(max_digits=12, decimal_places=2,
169 null=True, blank=True)
170 comp_ong_max = models.DecimalField(max_digits=12, decimal_places=2,
171 null=True, blank=True)
172 comp_autre_min = models.DecimalField(max_digits=12, decimal_places=2,
173 null=True, blank=True)
174 comp_autre_max = models.DecimalField(max_digits=12, decimal_places=2,
175 null=True, blank=True)
176
177 # Justification
178 justification = models.TextField(null=True, blank=True)
179
180 # Autres Metadata
181 date_validation = models.DateTimeField(null=True, blank=True) # de dae
182 date_debut = models.DateField(verbose_name=u"Date de début",
183 null=True, blank=True)
184 date_fin = models.DateField(verbose_name=u"Date de fin",
185 null=True, blank=True)
186
187 class Meta:
188 abstract = True
189 ordering = ['implantation__nom', 'nom']
190 verbose_name = u"Poste"
191 verbose_name_plural = u"Postes"
192
193 def __unicode__(self):
194 representation = u'%s - %s [%s]' % (self.implantation, self.nom,
195 self.id)
196 if self.is_vacant():
197 representation = representation + u' (VACANT)'
198 return representation
199
200 def is_vacant(self):
201 vacant = True
202 if self.occupe_par():
203 vacant = False
204 return vacant
205
206 def occupe_par(self):
207 """Retourne la liste d'employé occupant ce poste.
208 Généralement, retourne une liste d'un élément.
209 Si poste inoccupé, retourne liste vide.
210 """
211 return [d.employe for d in self.dossiers.filter(actif=True, supprime=False) \
212 .exclude(date_fin__lt=date.today())]
213
214 prefix_implantation = "implantation__region"
215 def get_regions(self):
216 return [self.implantation.region]
217
218
219 class Poste(Poste_):
220 __doc__ = Poste_.__doc__
221
222
223 POSTE_FINANCEMENT_CHOICES = (
224 ('A', 'A - Frais de personnel'),
225 ('B', 'B - Projet(s)-Titre(s)'),
226 ('C', 'C - Autre')
227 )
228
229
230 class PosteFinancement_(models.Model):
231 """Pour un Poste, structure d'informations décrivant comment on prévoit
232 financer ce Poste.
233 """
234 poste = models.ForeignKey('Poste', db_column='poste',
235 related_name='%(app_label)s_financements')
236 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
237 pourcentage = models.DecimalField(max_digits=12, decimal_places=2,
238 help_text="ex.: 33.33 % (décimale avec point)")
239 commentaire = models.TextField(
240 help_text="Spécifiez la source de financement.")
241
242 class Meta:
243 abstract = True
244 ordering = ['type']
245
246 def __unicode__(self):
247 return u'%s : %s %' % (self.type, self.pourcentage)
248
249
250 class PosteFinancement(PosteFinancement_):
251 __doc__ = PosteFinancement_.__doc__
252
253
254 class PostePiece(models.Model):
255 """Documents relatifs au Poste.
256 Ex.: Description de poste
257 """
258 poste = models.ForeignKey('Poste', db_column='poste',
259 related_name='pieces')
260 nom = models.CharField(verbose_name = u"Nom", max_length=255)
261 fichier = models.FileField(verbose_name = u"Fichier",
262 upload_to=poste_piece_dispatch,
263 storage=storage_prive)
264
265 class Meta:
266 ordering = ['nom']
267
268 def __unicode__(self):
269 return u'%s' % (self.nom)
270
271 class PosteComparaison(models.Model):
272 """
273 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
274 """
275 poste = models.ForeignKey('Poste', related_name='comparaisons_internes')
276 implantation = models.ForeignKey(ref.Implantation, null=True, blank=True, related_name="+")
277 nom = models.CharField(verbose_name = u"Poste", max_length=255, null=True, blank=True)
278 montant = models.IntegerField(null=True)
279 devise = models.ForeignKey("Devise", default=5, related_name='+', null=True, blank=True)
280
281 def taux_devise(self):
282 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.implantation)
283 if len(liste_taux) == 0:
284 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.implantation))
285 else:
286 return liste_taux[0].taux
287
288 def montant_euros(self):
289 return round(float(self.montant) * float(self.taux_devise()), 2)
290
291
292 class PosteCommentaire(Commentaire):
293 poste = models.ForeignKey('Poste', db_column='poste', related_name='+')
294
295
296 ### EMPLOYÉ/PERSONNE
297
298 GENRE_CHOICES = (
299 ('M', 'Homme'),
300 ('F', 'Femme'),
301 )
302 SITUATION_CHOICES = (
303 ('C', 'Célibataire'),
304 ('F', 'Fiancé'),
305 ('M', 'Marié'),
306 )
307
308 class Employe(AUFMetadata):
309 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
310 Dossiers qu'il occupe ou a occupé de Postes.
311
312 Cette classe aurait pu avantageusement s'appeler Personne car la notion
313 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
314 """
315 # Identification
316 nom = models.CharField(max_length=255)
317 prenom = models.CharField(max_length=255, verbose_name = u"Prénom")
318 nom_affichage = models.CharField(max_length=255,
319 verbose_name = u"Nom d'affichage",
320 null=True, blank=True)
321 nationalite = models.ForeignKey(ref.Pays, to_field='code',
322 db_column='nationalite',
323 related_name='employes_nationalite',
324 verbose_name = u"Nationalité",
325 blank=True, null=True)
326 date_naissance = models.DateField(verbose_name = u"Date de naissance",
327 validators=[validate_date_passee],
328 null=True, blank=True)
329 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
330
331 # Infos personnelles
332 situation_famille = models.CharField(max_length=1,
333 choices=SITUATION_CHOICES,
334 verbose_name = u"Situation familiale",
335 null=True, blank=True)
336 date_entree = models.DateField(verbose_name = u"Date d'entrée à l'AUF",
337 null=True, blank=True)
338
339 # Coordonnées
340 tel_domicile = models.CharField(max_length=255,
341 verbose_name = u"Tél. domicile",
342 null=True, blank=True)
343 tel_cellulaire = models.CharField(max_length=255,
344 verbose_name = u"Tél. cellulaire",
345 null=True, blank=True)
346 adresse = models.CharField(max_length=255, null=True, blank=True)
347 ville = models.CharField(max_length=255, null=True, blank=True)
348 province = models.CharField(max_length=255, null=True, blank=True)
349 code_postal = models.CharField(max_length=255, null=True, blank=True)
350 pays = models.ForeignKey(ref.Pays, to_field='code', db_column='pays',
351 related_name='employes',
352 null=True, blank=True)
353
354 class Meta:
355 ordering = ['nom_affichage','nom','prenom']
356 verbose_name = u"Employé"
357 verbose_name_plural = u"Employés"
358
359 def __unicode__(self):
360 return u'%s [%s]' % (self.get_nom(), self.id)
361
362 def get_nom(self):
363 nom_affichage = self.nom_affichage
364 if not nom_affichage:
365 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
366 return nom_affichage
367
368 def civilite(self):
369 civilite = u''
370 if self.genre.upper() == u'M':
371 civilite = u'M.'
372 elif self.genre.upper() == u'F':
373 civilite = u'Mme'
374 return civilite
375
376 def url_photo(self):
377 """Retourne l'URL du service retournant la photo de l'Employe.
378 Équivalent reverse url 'rh_photo' avec id en param.
379 """
380 from django.core.urlresolvers import reverse
381 return reverse('rh_photo', kwargs={'id':self.id})
382
383 def dossiers_passes(self):
384 today = date.today()
385 dossiers_passes = self.dossiers.filter(date_fin__lt=today).order_by('-date_fin')
386 for d in dossiers_passes:
387 d.archive = True
388 return dossiers_passes
389
390 def dossiers_futurs(self):
391 today = date.today()
392 return self.dossiers.filter(date_debut__gt=today).order_by('-date_fin')
393
394 def dossiers_encours(self):
395 dossiers_p_f = self.dossiers_passes() | self.dossiers_futurs()
396 ids_dossiers_p_f = [d.id for d in dossiers_p_f]
397 dossiers_encours = self.dossiers.exclude(id__in=ids_dossiers_p_f).order_by('-date_fin')
398
399 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
400 for d in dossiers_encours:
401 d.remunerations = Remuneration.objects.filter(dossier=d.id).order_by('-id')
402 return dossiers_encours
403
404 def postes_encours(self):
405 postes_encours = set()
406 for d in self.dossiers_encours():
407 postes_encours.add(d.poste)
408 return postes_encours
409
410 def poste_principal(self):
411 """
412 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
413 Idée derrière :
414 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
415 """
416 poste = Poste.objects.none()
417 try:
418 poste = self.dossiers_encours().order_by('date_debut')[0].poste
419 except:
420 pass
421 return poste
422
423 prefix_implantation = "dossiers__poste__implantation__region"
424 def get_regions(self):
425 regions = []
426 for d in self.dossiers.all():
427 regions.append(d.poste.implantation.region)
428 return regions
429
430
431 class EmployePiece(models.Model):
432 """Documents relatifs à un employé.
433 Ex.: CV...
434 """
435 employe = models.ForeignKey('Employe', db_column='employe')
436 nom = models.CharField(verbose_name="Nom", max_length=255)
437 fichier = models.FileField(verbose_name="Fichier",
438 upload_to=employe_piece_dispatch,
439 storage=storage_prive)
440
441 class Meta:
442 ordering = ['nom']
443 verbose_name = u"Employé pièce"
444 verbose_name_plural = u"Employé pièces"
445
446 def __unicode__(self):
447 return u'%s' % (self.nom)
448
449 class EmployeCommentaire(Commentaire):
450 employe = models.ForeignKey('Employe', db_column='employe',
451 related_name='+')
452
453 class Meta:
454 verbose_name = u"Employé commentaire"
455 verbose_name_plural = u"Employé commentaires"
456
457
458 LIEN_PARENTE_CHOICES = (
459 ('Conjoint', 'Conjoint'),
460 ('Conjointe', 'Conjointe'),
461 ('Fille', 'Fille'),
462 ('Fils', 'Fils'),
463 )
464
465 class AyantDroit(AUFMetadata):
466 """Personne en relation avec un Employe.
467 """
468 # Identification
469 nom = models.CharField(max_length=255)
470 prenom = models.CharField(max_length=255,
471 verbose_name = u"Prénom",)
472 nom_affichage = models.CharField(max_length=255,
473 verbose_name = u"Nom d'affichage",
474 null=True, blank=True)
475 nationalite = models.ForeignKey(ref.Pays, to_field='code',
476 db_column='nationalite',
477 related_name='ayantdroits_nationalite',
478 verbose_name = u"Nationalité",
479 null=True, blank=True)
480 date_naissance = models.DateField(verbose_name = u"Date de naissance",
481 validators=[validate_date_passee],
482 null=True, blank=True)
483 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
484
485 # Relation
486 employe = models.ForeignKey('Employe', db_column='employe',
487 related_name='ayantdroits',
488 verbose_name = u"Employé")
489 lien_parente = models.CharField(max_length=10,
490 choices=LIEN_PARENTE_CHOICES,
491 verbose_name = u"Lien de parenté",
492 null=True, blank=True)
493
494 class Meta:
495 ordering = ['nom_affichage']
496 verbose_name = u"Ayant droit"
497 verbose_name_plural = u"Ayants droit"
498
499 def __unicode__(self):
500 return u'%s' % (self.get_nom())
501
502 def get_nom(self):
503 nom_affichage = self.nom_affichage
504 if not nom_affichage:
505 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
506 return nom_affichage
507
508 prefix_implantation = "employe__dossiers__poste__implantation__region"
509 def get_regions(self):
510 regions = []
511 for d in self.employe.dossiers.all():
512 regions.append(d.poste.implantation.region)
513 return regions
514
515
516 class AyantDroitCommentaire(Commentaire):
517 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
518 related_name='+')
519
520
521 ### DOSSIER
522
523 STATUT_RESIDENCE_CHOICES = (
524 ('local', 'Local'),
525 ('expat', 'Expatrié'),
526 )
527
528 COMPTE_COMPTA_CHOICES = (
529 ('coda', 'CODA'),
530 ('scs', 'SCS'),
531 ('aucun', 'Aucun'),
532 )
533
534 class Dossier_(AUFMetadata):
535 """Le Dossier regroupe les informations relatives à l'occupation
536 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
537 par un Employe.
538
539 Plusieurs Contrats peuvent être associés au Dossier.
540 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
541 lequel aucun Dossier n'existe est un poste vacant.
542 """
543 # Identification
544 employe = models.ForeignKey('Employe', db_column='employe',
545 related_name='dossiers',
546 verbose_name=u"Employé")
547 # TODO: OneToOne ??
548 poste = models.ForeignKey('Poste', db_column='poste', related_name='dossiers')
549 statut = models.ForeignKey('Statut', related_name='+', default=3,
550 null=True)
551 organisme_bstg = models.ForeignKey('OrganismeBstg',
552 db_column='organisme_bstg',
553 related_name='+',
554 verbose_name = u"Organisme",
555 help_text="Si détaché (DET) ou \
556 mis à disposition (MAD), \
557 préciser l'organisme.",
558 null=True, blank=True)
559
560 # Recrutement
561 remplacement = models.BooleanField(default=False)
562 remplacement_de = models.ForeignKey('self', related_name='+',
563 null=True, blank=True)
564 statut_residence = models.CharField(max_length=10, default='local',
565 verbose_name = u"Statut", null=True,
566 choices=STATUT_RESIDENCE_CHOICES)
567
568 # Rémunération
569 classement = models.ForeignKey('Classement', db_column='classement',
570 related_name='+',
571 null=True, blank=True)
572 regime_travail = models.DecimalField(max_digits=12, null=True,
573 decimal_places=2,
574 default=REGIME_TRAVAIL_DEFAULT,
575 verbose_name = u"Régime de travail",
576 help_text="% du temps complet")
577 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
578 decimal_places=2, null=True,
579 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
580 verbose_name = u"Nb. heures par semaine")
581
582 # Occupation du Poste par cet Employe (anciennement "mandat")
583 date_debut = models.DateField(verbose_name = u"Date de début d'occupation \
584 de poste",)
585 date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \
586 de poste",
587 null=True, blank=True)
588
589 # Comptes
590 # TODO?
591
592 class Meta:
593 abstract = True
594 ordering = ['employe__nom', ]
595 verbose_name = u"Dossier"
596 verbose_name_plural = "Dossiers"
597
598 def salaire_theorique(self):
599 annee = date.today().year
600 coeff = self.classement.coefficient
601 implantation = self.poste.implantation
602 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
603
604 montant = coeff * point.valeur
605 devise = point.devise
606 return {'montant':montant, 'devise':devise}
607
608 def __unicode__(self):
609 poste = self.poste.nom
610 if self.employe.genre == 'F':
611 poste = self.poste.nom_feminin
612 return u'%s - %s' % (self.employe, poste)
613
614 prefix_implantation = "poste__implantation__region"
615 def get_regions(self):
616 return [self.poste.implantation.region]
617
618
619 def remunerations(self):
620 return self.rh_remuneration_remunerations.all().order_by('date_debut')
621
622 def get_salaire(self):
623 try:
624 return [r for r in self.remunerations().order_by('-date_debut') if r.type_id == 1][0]
625 except:
626 return None
627
628 class Dossier(Dossier_):
629 __doc__ = Dossier_.__doc__
630
631
632 class DossierPiece(models.Model):
633 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
634 Ex.: Lettre de motivation.
635 """
636 dossier = models.ForeignKey('Dossier', db_column='dossier',
637 related_name='+')
638 nom = models.CharField(verbose_name = u"Nom", max_length=255)
639 fichier = models.FileField(verbose_name = u"Fichier",
640 upload_to=dossier_piece_dispatch,
641 storage=storage_prive)
642
643 class Meta:
644 ordering = ['nom']
645
646 def __unicode__(self):
647 return u'%s' % (self.nom)
648
649 class DossierCommentaire(Commentaire):
650 dossier = models.ForeignKey('Dossier', db_column='dossier',
651 related_name='+')
652
653 class DossierComparaison(models.Model):
654 """
655 Photo d'une comparaison salariale au moment de l'embauche.
656 """
657 dossier = models.ForeignKey('Dossier', related_name='comparaisons')
658 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
659 poste = models.CharField(max_length=255, null=True, blank=True)
660 personne = models.CharField(max_length=255, null=True, blank=True)
661 montant = models.IntegerField(null=True)
662 devise = models.ForeignKey('Devise', default=5, related_name='+', null=True, blank=True)
663
664 def taux_devise(self):
665 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.dossier.poste.implantation)
666 if len(liste_taux) == 0:
667 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.dossier.poste.implantation))
668 else:
669 return liste_taux[0].taux
670
671 def montant_euros(self):
672 return round(float(self.montant) * float(self.taux_devise()), 2)
673
674
675 ### RÉMUNÉRATION
676
677 class RemunerationMixin(AUFMetadata):
678 # Identification
679 dossier = models.ForeignKey('Dossier', db_column='dossier',
680 related_name='%(app_label)s_%(class)s_remunerations')
681 type = models.ForeignKey('TypeRemuneration', db_column='type',
682 related_name='+',
683 verbose_name = u"Type de rémunération")
684 type_revalorisation = models.ForeignKey('TypeRevalorisation',
685 db_column='type_revalorisation',
686 related_name='+',
687 verbose_name = u"Type de revalorisation",
688 null=True, blank=True)
689 montant = models.FloatField(null=True, blank=True,
690 default=0)
691 # Annuel (12 mois, 52 semaines, 364 jours?)
692 devise = models.ForeignKey('Devise', to_field='id',
693 db_column='devise', related_name='+',
694 default=5)
695 # commentaire = precision
696 commentaire = models.CharField(max_length=255, null=True, blank=True)
697 # date_debut = anciennement date_effectif
698 date_debut = models.DateField(verbose_name = u"Date de début",
699 null=True, blank=True)
700 date_fin = models.DateField(verbose_name = u"Date de fin",
701 null=True, blank=True)
702
703 class Meta:
704 abstract = True
705 ordering = ['type__nom', '-date_fin']
706
707 def __unicode__(self):
708 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
709
710 class Remuneration_(RemunerationMixin):
711 """Structure de rémunération (données budgétaires) en situation normale
712 pour un Dossier. Si un Evenement existe, utiliser la structure de
713 rémunération EvenementRemuneration de cet événement.
714 """
715
716 def montant_mois(self):
717 return round(self.montant / 12, 2)
718
719 def taux_devise(self):
720 return self.devise.tauxchange_set.order_by('-annee').all()[0].taux
721
722 def montant_euro(self):
723 return round(float(self.montant) / float(self.taux_devise()), 2)
724
725 def montant_euro_mois(self):
726 return round(self.montant_euro() / 12, 2)
727
728 def __unicode__(self):
729 try:
730 devise = self.devise.code
731 except:
732 devise = "???"
733 return "%s %s" % (self.montant, devise)
734
735 class Meta:
736 abstract = True
737 verbose_name = u"Rémunération"
738 verbose_name_plural = u"Rémunérations"
739
740
741 class Remuneration(Remuneration_):
742 __doc__ = Remuneration_.__doc__
743
744
745 ### CONTRATS
746
747 class ContratManager(NoDeleteManager):
748 def get_query_set(self):
749 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
750
751
752 class Contrat(AUFMetadata):
753 """Document juridique qui encadre la relation de travail d'un Employe
754 pour un Poste particulier. Pour un Dossier (qui documente cette
755 relation de travail) plusieurs contrats peuvent être associés.
756 """
757
758 objects = ContratManager()
759
760 dossier = models.ForeignKey('Dossier', db_column='dossier',
761 related_name='contrats')
762 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
763 related_name='+',
764 verbose_name = u"Type de contrat")
765 date_debut = models.DateField(verbose_name = u"Date de début")
766 date_fin = models.DateField(verbose_name = u"Date de fin",
767 null=True, blank=True)
768
769 class Meta:
770 ordering = ['dossier__employe__nom_affichage']
771 verbose_name = u"Contrat"
772 verbose_name_plural = u"Contrats"
773
774 def __unicode__(self):
775 return u'%s - %s' % (self.dossier, self.id)
776
777 # TODO? class ContratPiece(models.Model):
778
779
780 ### ÉVÉNEMENTS
781
782 class Evenement_(AUFMetadata):
783 """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
784 d'un Dossier qui vient altérer des informations normales liées à un Dossier
785 (ex.: la Remuneration).
786
787 Ex.: congé de maternité, maladie...
788
789 Lors de ces situations exceptionnelles, l'Employe a un régime de travail
790 différent et une rémunération en conséquence. On souhaite toutefois
791 conserver le Dossier intact afin d'éviter une re-saisie des données lors
792 du retour à la normale.
793 """
794 dossier = models.ForeignKey('Dossier', db_column='dossier',
795 related_name='+')
796 nom = models.CharField(max_length=255)
797 date_debut = models.DateField(verbose_name = u"Date de début")
798 date_fin = models.DateField(verbose_name = u"Date de fin",
799 null=True, blank=True)
800
801 class Meta:
802 abstract = True
803 ordering = ['nom']
804 verbose_name = u"Évènement"
805 verbose_name_plural = u"Évènements"
806
807 def __unicode__(self):
808 return u'%s' % (self.nom)
809
810
811 class Evenement(Evenement_):
812 __doc__ = Evenement_.__doc__
813
814
815 class EvenementRemuneration_(RemunerationMixin):
816 """Structure de rémunération liée à un Evenement qui remplace
817 temporairement la Remuneration normale d'un Dossier, pour toute la durée
818 de l'Evenement.
819 """
820 evenement = models.ForeignKey("Evenement", db_column='evenement',
821 related_name='+',
822 verbose_name = u"Évènement")
823 # TODO : le champ dossier hérité de Remuneration doit être dérivé
824 # de l'Evenement associé
825
826 class Meta:
827 abstract = True
828 ordering = ['evenement', 'type__nom', '-date_fin']
829 verbose_name = u"Évènement - rémunération"
830 verbose_name_plural = u"Évènements - rémunérations"
831
832
833 class EvenementRemuneration(EvenementRemuneration_):
834 __doc__ = EvenementRemuneration_.__doc__
835
836 class Meta:
837 abstract = True
838
839
840 class EvenementRemuneration(EvenementRemuneration_):
841 __doc__ = EvenementRemuneration_.__doc__
842
843
844 ### RÉFÉRENCES RH
845
846 class FamilleEmploi(AUFMetadata):
847 """Catégorie utilisée dans la gestion des Postes.
848 Catégorie supérieure à TypePoste.
849 """
850 nom = models.CharField(max_length=255)
851
852 class Meta:
853 ordering = ['nom']
854 verbose_name = u"Famille d'emploi"
855 verbose_name_plural = u"Familles d'emploi"
856
857 def __unicode__(self):
858 return u'%s' % (self.nom)
859
860 class TypePoste(AUFMetadata):
861 """Catégorie de Poste.
862 """
863 nom = models.CharField(max_length=255)
864 nom_feminin = models.CharField(max_length=255,
865 verbose_name = u"Nom féminin")
866
867 is_responsable = models.BooleanField(default=False,
868 verbose_name = u"Poste de responsabilité")
869 famille_emploi = models.ForeignKey('FamilleEmploi',
870 db_column='famille_emploi',
871 related_name='+',
872 verbose_name = u"Famille d'emploi")
873
874 class Meta:
875 ordering = ['nom']
876 verbose_name = u"Type de poste"
877 verbose_name_plural = u"Types de poste"
878
879 def __unicode__(self):
880 return u'%s' % (self.nom)
881
882
883 TYPE_PAIEMENT_CHOICES = (
884 ('Régulier', 'Régulier'),
885 ('Ponctuel', 'Ponctuel'),
886 )
887
888 NATURE_REMUNERATION_CHOICES = (
889 ('Accessoire', 'Accessoire'),
890 ('Charges', 'Charges'),
891 ('Indemnité', 'Indemnité'),
892 ('RAS', 'Rémunération autre source'),
893 ('Traitement', 'Traitement'),
894 )
895
896 class TypeRemuneration(AUFMetadata):
897 """Catégorie de Remuneration.
898 """
899 nom = models.CharField(max_length=255)
900 type_paiement = models.CharField(max_length=30,
901 choices=TYPE_PAIEMENT_CHOICES,
902 verbose_name = u"Type de paiement")
903 nature_remuneration = models.CharField(max_length=30,
904 choices=NATURE_REMUNERATION_CHOICES,
905 verbose_name = u"Nature de la rémunération")
906
907 class Meta:
908 ordering = ['nom']
909 verbose_name = u"Type de rémunération"
910 verbose_name_plural = u"Types de rémunération"
911
912 def __unicode__(self):
913 return u'%s' % (self.nom)
914
915 class TypeRevalorisation(AUFMetadata):
916 """Justification du changement de la Remuneration.
917 (Actuellement utilisé dans aucun traitement informatique.)
918 """
919 nom = models.CharField(max_length=255)
920
921 class Meta:
922 ordering = ['nom']
923 verbose_name = u"Type de revalorisation"
924 verbose_name_plural = u"Types de revalorisation"
925
926 def __unicode__(self):
927 return u'%s' % (self.nom)
928
929 class Service(AUFMetadata):
930 """Unité administrative où les Postes sont rattachés.
931 """
932 nom = models.CharField(max_length=255)
933
934 class Meta:
935 ordering = ['nom']
936 verbose_name = u"Service"
937 verbose_name_plural = u"Services"
938
939 def __unicode__(self):
940 return u'%s' % (self.nom)
941
942
943 TYPE_ORGANISME_CHOICES = (
944 ('MAD', 'Mise à disposition'),
945 ('DET', 'Détachement'),
946 )
947
948 class OrganismeBstg(AUFMetadata):
949 """Organisation d'où provient un Employe mis à disposition (MAD) de
950 ou détaché (DET) à l'AUF à titre gratuit.
951
952 (BSTG = bien et service à titre gratuit.)
953 """
954 nom = models.CharField(max_length=255)
955 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
956 pays = models.ForeignKey(ref.Pays, to_field='code',
957 db_column='pays',
958 related_name='organismes_bstg',
959 null=True, blank=True)
960
961 class Meta:
962 ordering = ['type', 'nom']
963 verbose_name = u"Organisme BSTG"
964 verbose_name_plural = u"Organismes BSTG"
965
966 def __unicode__(self):
967 return u'%s (%s)' % (self.nom, self.get_type_display())
968
969 prefix_implantation = "pays__region"
970 def get_regions(self):
971 return [self.pays.region]
972
973
974 class Statut(AUFMetadata):
975 """Statut de l'Employe dans le cadre d'un Dossier particulier.
976 """
977 # Identification
978 code = models.CharField(max_length=25, unique=True, help_text="Saisir un code court mais lisible pour ce statut : le code est utilisé pour associer les statuts aux autres données tout en demeurant plus lisible qu'un identifiant numérique.")
979 nom = models.CharField(max_length=255)
980
981 class Meta:
982 ordering = ['code']
983 verbose_name = u"Statut d'employé"
984 verbose_name_plural = u"Statuts d'employé"
985
986 def __unicode__(self):
987 return u'%s : %s' % (self.code, self.nom)
988
989
990 TYPE_CLASSEMENT_CHOICES = (
991 ('S', 'S -Soutien'),
992 ('T', 'T - Technicien'),
993 ('P', 'P - Professionel'),
994 ('C', 'C - Cadre'),
995 ('D', 'D - Direction'),
996 ('SO', 'SO - Sans objet [expatriés]'),
997 ('HG', 'HG - Hors grille [direction]'),
998 )
999
1000
1001 class Classement_(AUFMetadata):
1002 """Éléments de classement de la
1003 "Grille générique de classement hiérarchique".
1004
1005 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1006 classement dans la grille. Le classement donne le coefficient utilisé dans:
1007
1008 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1009 """
1010 # Identification
1011 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1012 echelon = models.IntegerField(verbose_name=u"Échelon", blank=True, default=0)
1013 degre = models.IntegerField(verbose_name=u"Degré", blank=True, default=0)
1014 coefficient = models.FloatField(default=0, verbose_name=u"Coefficient",
1015 null=True)
1016 # Méta
1017 # annee # au lieu de date_debut et date_fin
1018 commentaire = models.TextField(null=True, blank=True)
1019
1020 class Meta:
1021 abstract = True
1022 ordering = ['type','echelon','degre','coefficient']
1023 verbose_name = u"Classement"
1024 verbose_name_plural = u"Classements"
1025
1026 def __unicode__(self):
1027 return u'%s.%s.%s (%s)' % (self.type, self.echelon, self.degre,
1028 self.coefficient)
1029
1030 class Classement(Classement_):
1031 __doc__ = Classement_.__doc__
1032
1033
1034 class TauxChange_(AUFMetadata):
1035 """Taux de change de la devise vers l'euro (EUR)
1036 pour chaque année budgétaire.
1037 """
1038 # Identification
1039 devise = models.ForeignKey('Devise', db_column='devise')
1040 annee = models.IntegerField(verbose_name = u"Année")
1041 taux = models.FloatField(verbose_name = u"Taux vers l'euro")
1042
1043 class Meta:
1044 abstract = True
1045 ordering = ['-annee', 'devise__code']
1046 verbose_name = u"Taux de change"
1047 verbose_name_plural = u"Taux de change"
1048
1049 def __unicode__(self):
1050 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1051
1052
1053 class TauxChange(TauxChange_):
1054 __doc__ = TauxChange_.__doc__
1055
1056 class ValeurPointManager(NoDeleteManager):
1057 def get_query_set(self):
1058 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1059
1060
1061 class ValeurPoint_(AUFMetadata):
1062 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1063 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1064 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1065
1066 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1067 """
1068
1069 actuelles = ValeurPointManager()
1070
1071 valeur = models.FloatField(null=True)
1072 devise = models.ForeignKey('Devise', db_column='devise', null=True,
1073 related_name='+', default=5)
1074 implantation = models.ForeignKey(ref.Implantation,
1075 db_column='implantation',
1076 related_name='%(app_label)s_valeur_point')
1077 # Méta
1078 annee = models.IntegerField()
1079
1080 class Meta:
1081 ordering = ['-annee', 'implantation__nom']
1082 abstract = True
1083 verbose_name = u"Valeur du point"
1084 verbose_name_plural = u"Valeurs du point"
1085
1086 # TODO : cette fonction n'était pas présente dans la branche dev, utilité?
1087 def get_tauxchange_courant(self):
1088 """
1089 Recherche le taux courant associé à la valeur d'un point.
1090 Tous les taux de l'année courante sont chargés, pour optimiser un
1091 affichage en liste. (On pourrait probablement améliorer le manager pour
1092 lui greffer le taux courant sous forme de JOIN)
1093 """
1094 for tauxchange in self.tauxchange:
1095 if tauxchange.implantation_id == self.implantation_id:
1096 return tauxchange
1097 return None
1098
1099 def __unicode__(self):
1100 return u'%s %s (%s)' % (self.valeur, self.devise, self.annee)
1101
1102
1103 class ValeurPoint(ValeurPoint_):
1104 __doc__ = ValeurPoint_.__doc__
1105
1106
1107 class Devise(AUFMetadata):
1108 """Devise monétaire.
1109 """
1110 code = models.CharField(max_length=10, unique=True)
1111 nom = models.CharField(max_length=255)
1112
1113 class Meta:
1114 ordering = ['code']
1115 verbose_name = u"Devise"
1116 verbose_name_plural = u"Devises"
1117
1118 def __unicode__(self):
1119 return u'%s - %s' % (self.code, self.nom)
1120
1121 class TypeContrat(AUFMetadata):
1122 """Type de contrat.
1123 """
1124 nom = models.CharField(max_length=255)
1125 nom_long = models.CharField(max_length=255)
1126
1127 class Meta:
1128 ordering = ['nom']
1129 verbose_name = u"Type de contrat"
1130 verbose_name_plural = u"Types de contrat"
1131
1132 def __unicode__(self):
1133 return u'%s' % (self.nom)
1134
1135
1136 ### AUTRES
1137
1138 class ResponsableImplantation(AUFMetadata):
1139 """Le responsable d'une implantation.
1140 Anciennement géré sur le Dossier du responsable.
1141 """
1142 employe = models.ForeignKey('Employe', db_column='employe',
1143 related_name='+',
1144 null=True, blank=True)
1145 implantation = models.ForeignKey(ref.Implantation,
1146 db_column='implantation', related_name='+',
1147 unique=True)
1148
1149 def __unicode__(self):
1150 return u'%s : %s' % (self.implantation, self.employe)
1151
1152 class Meta:
1153 ordering = ['implantation__nom']
1154 verbose_name = "Responsable d'implantation"
1155 verbose_name_plural = "Responsables d'implantation"