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