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