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