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