50f5e0f067142389eb894884b0f16a8b38485645
[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 [%s]' % (self.get_nom(), self.id)
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 EmployeInactif(Employe):
420 class Meta:
421 proxy = True
422 ordering = ['nom_affichage','nom','prenom']
423 verbose_name = u"Employé inactif"
424 verbose_name_plural = u"Employés inactifs"
425
426
427 class EmployePiece(models.Model):
428 """Documents relatifs à un employé.
429 Ex.: CV...
430 """
431 employe = models.ForeignKey('Employe', db_column='employe')
432 nom = models.CharField(verbose_name="Nom", max_length=255)
433 fichier = models.FileField(verbose_name="Fichier",
434 upload_to=employe_piece_dispatch,
435 storage=storage_prive)
436
437 class Meta:
438 ordering = ['nom']
439 verbose_name = u"Employé pièce"
440 verbose_name_plural = u"Employé pièces"
441
442 def __unicode__(self):
443 return u'%s' % (self.nom)
444
445 class EmployeCommentaire(Commentaire):
446 employe = models.ForeignKey('Employe', db_column='employe',
447 related_name='+')
448
449 class Meta:
450 verbose_name = u"Employé commentaire"
451 verbose_name_plural = u"Employé commentaires"
452
453
454 LIEN_PARENTE_CHOICES = (
455 ('Conjoint', 'Conjoint'),
456 ('Conjointe', 'Conjointe'),
457 ('Fille', 'Fille'),
458 ('Fils', 'Fils'),
459 )
460
461 class AyantDroit(AUFMetadata):
462 """Personne en relation avec un Employe.
463 """
464 # Identification
465 nom = models.CharField(max_length=255)
466 prenom = models.CharField(max_length=255,
467 verbose_name = u"Prénom",)
468 nom_affichage = models.CharField(max_length=255,
469 verbose_name = u"Nom d'affichage",
470 null=True, blank=True)
471 nationalite = models.ForeignKey(ref.Pays, to_field='code',
472 db_column='nationalite',
473 related_name='ayantdroits_nationalite',
474 verbose_name = u"Nationalité")
475 date_naissance = models.DateField(help_text=HELP_TEXT_DATE,
476 verbose_name = u"Date de naissance",
477 validators=[validate_date_passee],
478 null=True, blank=True)
479 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
480
481 # Relation
482 employe = models.ForeignKey('Employe', db_column='employe',
483 related_name='ayantdroits',
484 verbose_name = u"Employé")
485 lien_parente = models.CharField(max_length=10,
486 choices=LIEN_PARENTE_CHOICES,
487 verbose_name = u"Lien de parenté",
488 null=True, blank=True)
489
490 class Meta:
491 ordering = ['nom_affichage']
492 verbose_name = u"Ayant droit"
493 verbose_name_plural = u"Ayants droit"
494
495 def __unicode__(self):
496 return u'%s' % (self.get_nom())
497
498 def get_nom(self):
499 nom_affichage = self.nom_affichage
500 if not nom_affichage:
501 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
502 return nom_affichage
503
504 prefix_implantation = "employe__dossiers__poste__implantation__region"
505 def get_regions(self):
506 regions = []
507 for d in self.employe.dossiers.all():
508 regions.append(d.poste.implantation.region)
509 return regions
510
511
512 class AyantDroitCommentaire(Commentaire):
513 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
514 related_name='+')
515
516
517 ### DOSSIER
518
519 STATUT_RESIDENCE_CHOICES = (
520 ('local', 'Local'),
521 ('expat', 'Expatrié'),
522 )
523
524 COMPTE_COMPTA_CHOICES = (
525 ('coda', 'CODA'),
526 ('scs', 'SCS'),
527 ('aucun', 'Aucun'),
528 )
529
530 class Dossier_(AUFMetadata):
531 """Le Dossier regroupe les informations relatives à l'occupation
532 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
533 par un Employe.
534
535 Plusieurs Contrats peuvent être associés au Dossier.
536 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
537 lequel aucun Dossier n'existe est un poste vacant.
538 """
539 # Identification
540 employe = models.ForeignKey('Employe', db_column='employe',
541 related_name='dossiers',
542 verbose_name=u"Employé")
543 # TODO: OneToOne ??
544 poste = models.ForeignKey('Poste', db_column='poste', related_name='dossiers')
545 statut = models.ForeignKey('Statut', related_name='+', default=3,
546 null=True)
547 organisme_bstg = models.ForeignKey('OrganismeBstg',
548 db_column='organisme_bstg',
549 related_name='+',
550 verbose_name = u"Organisme",
551 help_text="Si détaché (DET) ou \
552 mis à disposition (MAD), \
553 préciser l'organisme.",
554 null=True, blank=True)
555
556 # Recrutement
557 remplacement = models.BooleanField(default=False)
558 remplacement_de = models.ForeignKey('self', related_name='+',
559 null=True, blank=True)
560 statut_residence = models.CharField(max_length=10, default='local',
561 verbose_name = u"Statut", null=True,
562 choices=STATUT_RESIDENCE_CHOICES)
563
564 # Rémunération
565 classement = models.ForeignKey('Classement', db_column='classement',
566 related_name='+',
567 null=True, blank=True)
568 regime_travail = models.DecimalField(max_digits=12, null=True,
569 decimal_places=2,
570 default=REGIME_TRAVAIL_DEFAULT,
571 verbose_name = u"Régime de travail",
572 help_text="% du temps complet")
573 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
574 decimal_places=2, null=True,
575 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
576 verbose_name = u"Nb. heures par semaine")
577
578 # Occupation du Poste par cet Employe (anciennement "mandat")
579 date_debut = models.DateField(verbose_name = u"Date de début d'occupation \
580 de poste",
581 help_text=HELP_TEXT_DATE)
582 date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \
583 de poste",
584 help_text=HELP_TEXT_DATE,
585 null=True, blank=True)
586
587 # Comptes
588 # TODO?
589
590 class Meta:
591 abstract = True
592 ordering = ['employe__nom', ]
593 verbose_name = u"Dossier"
594 verbose_name_plural = "Dossiers"
595
596 def salaire_theorique(self):
597 annee = date.today().year
598 coeff = self.classement.coefficient
599 implantation = self.poste.implantation
600 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
601
602 montant = coeff * point.valeur
603 devise = point.devise
604 return {'montant':montant, 'devise':devise}
605
606 def __unicode__(self):
607 poste = self.poste.nom
608 if self.employe.genre == 'F':
609 poste = self.poste.nom_feminin
610 return u'%s - %s' % (self.employe, poste)
611
612 prefix_implantation = "poste__implantation__region"
613 def get_regions(self):
614 return [self.poste.implantation.region]
615
616
617 class Dossier(Dossier_):
618 __doc__ = Dossier_.__doc__
619
620
621 class DossierInactif(Dossier):
622
623 class Meta:
624 proxy = True
625 ordering = ['employe__nom', ]
626 verbose_name = u"Dossier inactif"
627 verbose_name_plural = u"Dossiers inactifs"
628
629
630 class DossierPiece(models.Model):
631 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
632 Ex.: Lettre de motivation.
633 """
634 dossier = models.ForeignKey('Dossier', db_column='dossier',
635 related_name='+')
636 nom = models.CharField(verbose_name = u"Nom", max_length=255)
637 fichier = models.FileField(verbose_name = u"Fichier",
638 upload_to=dossier_piece_dispatch,
639 storage=storage_prive)
640
641 class Meta:
642 ordering = ['nom']
643
644 def __unicode__(self):
645 return u'%s' % (self.nom)
646
647 class DossierCommentaire(Commentaire):
648 dossier = models.ForeignKey('Dossier', db_column='dossier',
649 related_name='+')
650
651 class DossierComparaison(models.Model):
652 """
653 Photo d'une comparaison salariale au moment de l'embauche.
654 """
655 dossier = models.ForeignKey('Dossier', related_name='comparaisons')
656 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
657 poste = models.CharField(max_length=255, null=True, blank=True)
658 personne = models.CharField(max_length=255, null=True, blank=True)
659 montant = models.IntegerField(null=True)
660 devise = models.ForeignKey('Devise', default=5, related_name='+', null=True, blank=True)
661
662 def taux_devise(self):
663 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.dossier.poste.implantation)
664 if len(liste_taux) == 0:
665 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.dossier.poste.implantation))
666 else:
667 return liste_taux[0].taux
668
669 def montant_euros(self):
670 return round(float(self.montant) * float(self.taux_devise()), 2)
671
672
673 ### RÉMUNÉRATION
674
675 class RemunerationMixin(AUFMetadata):
676 # Identification
677 dossier = models.ForeignKey('Dossier', db_column='dossier',
678 related_name='%(app_label)s_%(class)s_remunerations')
679 type = models.ForeignKey('TypeRemuneration', db_column='type',
680 related_name='+',
681 verbose_name = u"Type de rémunération")
682 type_revalorisation = models.ForeignKey('TypeRevalorisation',
683 db_column='type_revalorisation',
684 related_name='+',
685 verbose_name = u"Type de revalorisation",
686 null=True, blank=True)
687 montant = models.FloatField(null=True, blank=True,
688 default=0)
689 # Annuel (12 mois, 52 semaines, 364 jours?)
690 devise = models.ForeignKey('Devise', to_field='id',
691 db_column='devise', related_name='+',
692 default=5)
693 # commentaire = precision
694 commentaire = models.CharField(max_length=255, null=True, blank=True)
695 # date_debut = anciennement date_effectif
696 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
697 verbose_name = u"Date de début",
698 null=True, blank=True)
699 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
700 verbose_name = u"Date de fin",
701 null=True, blank=True)
702
703 class Meta:
704 abstract = True
705 ordering = ['type__nom', '-date_fin']
706
707 def __unicode__(self):
708 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
709
710 class Remuneration_(RemunerationMixin):
711 """Structure de rémunération (données budgétaires) en situation normale
712 pour un Dossier. Si un Evenement existe, utiliser la structure de
713 rémunération EvenementRemuneration de cet événement.
714 """
715
716 def montant_mois(self):
717 return round(self.montant / 12, 2)
718
719 def taux_devise(self):
720 return self.devise.tauxchange_set.order_by('-annee').all()[0].taux
721
722 def montant_euro(self):
723 return round(float(self.montant) / float(self.taux_devise()), 2)
724
725 def montant_euro_mois(self):
726 return round(self.montant_euro() / 12, 2)
727
728 def __unicode__(self):
729 try:
730 devise = self.devise.code
731 except:
732 devise = "???"
733 return "%s %s" % (self.montant, devise)
734
735 class Meta:
736 abstract = True
737 verbose_name = u"Rémunération"
738 verbose_name_plural = u"Rémunérations"
739
740
741 class Remuneration(Remuneration_):
742 __doc__ = Remuneration_.__doc__
743
744
745 ### CONTRATS
746
747 class ContratManager(NoDeleteManager):
748 def get_query_set(self):
749 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
750
751
752 class Contrat(AUFMetadata):
753 """Document juridique qui encadre la relation de travail d'un Employe
754 pour un Poste particulier. Pour un Dossier (qui documente cette
755 relation de travail) plusieurs contrats peuvent être associés.
756 """
757
758 objects = ContratManager()
759
760 dossier = models.ForeignKey('Dossier', db_column='dossier',
761 related_name='contrats')
762 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
763 related_name='+',
764 verbose_name = u"Type de contrat")
765 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
766 verbose_name = u"Date de début")
767 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
768 verbose_name = u"Date de fin",
769 null=True, blank=True)
770
771 class Meta:
772 ordering = ['dossier__employe__nom_affichage']
773 verbose_name = u"Contrat"
774 verbose_name_plural = u"Contrats"
775
776 def __unicode__(self):
777 return u'%s - %s' % (self.dossier, self.id)
778
779 # TODO? class ContratPiece(models.Model):
780
781
782 ### ÉVÉNEMENTS
783
784 class Evenement_(AUFMetadata):
785 """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
786 d'un Dossier qui vient altérer des informations normales liées à un Dossier
787 (ex.: la Remuneration).
788
789 Ex.: congé de maternité, maladie...
790
791 Lors de ces situations exceptionnelles, l'Employe a un régime de travail
792 différent et une rémunération en conséquence. On souhaite toutefois
793 conserver le Dossier intact afin d'éviter une re-saisie des données lors
794 du retour à la normale.
795 """
796 dossier = models.ForeignKey('Dossier', db_column='dossier',
797 related_name='+')
798 nom = models.CharField(max_length=255)
799 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
800 verbose_name = u"Date de début")
801 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
802 verbose_name = u"Date de fin",
803 null=True, blank=True)
804
805 class Meta:
806 abstract = True
807 ordering = ['nom']
808 verbose_name = u"Évènement"
809 verbose_name_plural = u"Évènements"
810
811 def __unicode__(self):
812 return u'%s' % (self.nom)
813
814
815 class Evenement(Evenement_):
816 __doc__ = Evenement_.__doc__
817
818
819 class EvenementRemuneration_(RemunerationMixin):
820 """Structure de rémunération liée à un Evenement qui remplace
821 temporairement la Remuneration normale d'un Dossier, pour toute la durée
822 de l'Evenement.
823 """
824 evenement = models.ForeignKey("Evenement", db_column='evenement',
825 related_name='+',
826 verbose_name = u"Évènement")
827 # TODO : le champ dossier hérité de Remuneration doit être dérivé
828 # de l'Evenement associé
829
830 class Meta:
831 abstract = True
832 ordering = ['evenement', 'type__nom', '-date_fin']
833 verbose_name = u"Évènement - rémunération"
834 verbose_name_plural = u"Évènements - rémunérations"
835
836
837 class EvenementRemuneration(EvenementRemuneration_):
838 __doc__ = EvenementRemuneration_.__doc__
839
840 class Meta:
841 abstract = True
842
843
844 class EvenementRemuneration(EvenementRemuneration_):
845 __doc__ = EvenementRemuneration_.__doc__
846
847
848 ### RÉFÉRENCES RH
849
850 class FamilleEmploi(AUFMetadata):
851 """Catégorie utilisée dans la gestion des Postes.
852 Catégorie supérieure à TypePoste.
853 """
854 nom = models.CharField(max_length=255)
855
856 class Meta:
857 ordering = ['nom']
858 verbose_name = u"Famille d'emploi"
859 verbose_name_plural = u"Familles d'emploi"
860
861 def __unicode__(self):
862 return u'%s' % (self.nom)
863
864 class TypePoste(AUFMetadata):
865 """Catégorie de Poste.
866 """
867 nom = models.CharField(max_length=255)
868 nom_feminin = models.CharField(max_length=255,
869 verbose_name = u"Nom féminin")
870
871 is_responsable = models.BooleanField(default=False,
872 verbose_name = u"Poste de responsabilité")
873 famille_emploi = models.ForeignKey('FamilleEmploi',
874 db_column='famille_emploi',
875 related_name='+',
876 verbose_name = u"Famille d'emploi")
877
878 class Meta:
879 ordering = ['nom']
880 verbose_name = u"Type de poste"
881 verbose_name_plural = u"Types de poste"
882
883 def __unicode__(self):
884 return u'%s' % (self.nom)
885
886
887 TYPE_PAIEMENT_CHOICES = (
888 ('Régulier', 'Régulier'),
889 ('Ponctuel', 'Ponctuel'),
890 )
891
892 NATURE_REMUNERATION_CHOICES = (
893 ('Accessoire', 'Accessoire'),
894 ('Charges', 'Charges'),
895 ('Indemnité', 'Indemnité'),
896 ('RAS', 'Rémunération autre source'),
897 ('Traitement', 'Traitement'),
898 )
899
900 class TypeRemuneration(AUFMetadata):
901 """Catégorie de Remuneration.
902 """
903 nom = models.CharField(max_length=255)
904 type_paiement = models.CharField(max_length=30,
905 choices=TYPE_PAIEMENT_CHOICES,
906 verbose_name = u"Type de paiement")
907 nature_remuneration = models.CharField(max_length=30,
908 choices=NATURE_REMUNERATION_CHOICES,
909 verbose_name = u"Nature de la rémunération")
910
911 class Meta:
912 ordering = ['nom']
913 verbose_name = u"Type de rémunération"
914 verbose_name_plural = u"Types de rémunération"
915
916 def __unicode__(self):
917 return u'%s' % (self.nom)
918
919 class TypeRevalorisation(AUFMetadata):
920 """Justification du changement de la Remuneration.
921 (Actuellement utilisé dans aucun traitement informatique.)
922 """
923 nom = models.CharField(max_length=255)
924
925 class Meta:
926 ordering = ['nom']
927 verbose_name = u"Type de revalorisation"
928 verbose_name_plural = u"Types de revalorisation"
929
930 def __unicode__(self):
931 return u'%s' % (self.nom)
932
933 class Service(AUFMetadata):
934 """Unité administrative où les Postes sont rattachés.
935 """
936 nom = models.CharField(max_length=255)
937
938 class Meta:
939 ordering = ['nom']
940 verbose_name = u"Service"
941 verbose_name_plural = u"Services"
942
943 def __unicode__(self):
944 return u'%s' % (self.nom)
945
946
947 TYPE_ORGANISME_CHOICES = (
948 ('MAD', 'Mise à disposition'),
949 ('DET', 'Détachement'),
950 )
951
952 class OrganismeBstg(AUFMetadata):
953 """Organisation d'où provient un Employe mis à disposition (MAD) de
954 ou détaché (DET) à l'AUF à titre gratuit.
955
956 (BSTG = bien et service à titre gratuit.)
957 """
958 nom = models.CharField(max_length=255)
959 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
960 pays = models.ForeignKey(ref.Pays, to_field='code',
961 db_column='pays',
962 related_name='organismes_bstg',
963 null=True, blank=True)
964
965 class Meta:
966 ordering = ['type', 'nom']
967 verbose_name = u"Organisme BSTG"
968 verbose_name_plural = u"Organismes BSTG"
969
970 def __unicode__(self):
971 return u'%s (%s)' % (self.nom, self.get_type_display())
972
973 prefix_implantation = "pays__region"
974 def get_regions(self):
975 return [self.pays.region]
976
977
978 class Statut(AUFMetadata):
979 """Statut de l'Employe dans le cadre d'un Dossier particulier.
980 """
981 # Identification
982 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.")
983 nom = models.CharField(max_length=255)
984
985 class Meta:
986 ordering = ['code']
987 verbose_name = u"Statut d'employé"
988 verbose_name_plural = u"Statuts d'employé"
989
990 def __unicode__(self):
991 return u'%s : %s' % (self.code, self.nom)
992
993
994 TYPE_CLASSEMENT_CHOICES = (
995 ('S', 'S -Soutien'),
996 ('T', 'T - Technicien'),
997 ('P', 'P - Professionel'),
998 ('C', 'C - Cadre'),
999 ('D', 'D - Direction'),
1000 ('SO', 'SO - Sans objet [expatriés]'),
1001 ('HG', 'HG - Hors grille [direction]'),
1002 )
1003
1004
1005 class Classement_(AUFMetadata):
1006 """Éléments de classement de la
1007 "Grille générique de classement hiérarchique".
1008
1009 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1010 classement dans la grille. Le classement donne le coefficient utilisé dans:
1011
1012 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1013 """
1014 # Identification
1015 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1016 echelon = models.IntegerField(verbose_name = u"Échelon")
1017 degre = models.IntegerField(verbose_name = u"Degré")
1018 coefficient = models.FloatField(default=0, verbose_name = u"Coéfficient",
1019 null=True)
1020 # Méta
1021 # annee # au lieu de date_debut et date_fin
1022 commentaire = models.TextField(null=True, blank=True)
1023
1024 class Meta:
1025 abstract = True
1026 ordering = ['type','echelon','degre','coefficient']
1027 verbose_name = u"Classement"
1028 verbose_name_plural = u"Classements"
1029
1030 def __unicode__(self):
1031 return u'%s.%s.%s (%s)' % (self.type, self.echelon, self.degre,
1032 self.coefficient)
1033
1034 class Classement(Classement_):
1035 __doc__ = Classement_.__doc__
1036
1037
1038 class TauxChange_(AUFMetadata):
1039 """Taux de change de la devise vers l'euro (EUR)
1040 pour chaque année budgétaire.
1041 """
1042 # Identification
1043 devise = models.ForeignKey('Devise', db_column='devise')
1044 annee = models.IntegerField(verbose_name = u"Année")
1045 taux = models.FloatField(verbose_name = u"Taux vers l'euro")
1046
1047 class Meta:
1048 abstract = True
1049 ordering = ['-annee', 'devise__code']
1050 verbose_name = u"Taux de change"
1051 verbose_name_plural = u"Taux de change"
1052
1053 def __unicode__(self):
1054 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1055
1056
1057 class TauxChange(TauxChange_):
1058 __doc__ = TauxChange_.__doc__
1059
1060 class ValeurPointManager(NoDeleteManager):
1061 def get_query_set(self):
1062 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1063
1064
1065 class ValeurPoint_(AUFMetadata):
1066 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1067 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1068 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1069
1070 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1071 """
1072
1073 objects = ValeurPointManager()
1074
1075 valeur = models.FloatField(null=True)
1076 devise = models.ForeignKey('Devise', db_column='devise', null=True,
1077 related_name='+', default=5)
1078 implantation = models.ForeignKey(ref.Implantation,
1079 db_column='implantation',
1080 related_name='%(app_label)s_valeur_point')
1081 # Méta
1082 annee = models.IntegerField()
1083
1084 class Meta:
1085 ordering = ['-annee', 'implantation__nom']
1086 abstract = True
1087 verbose_name = u"Valeur du point"
1088 verbose_name_plural = u"Valeurs du point"
1089
1090 # TODO : cette fonction n'était pas présente dans la branche dev, utilité?
1091 def get_tauxchange_courant(self):
1092 """
1093 Recherche le taux courant associé à la valeur d'un point.
1094 Tous les taux de l'année courante sont chargés, pour optimiser un
1095 affichage en liste. (On pourrait probablement améliorer le manager pour
1096 lui greffer le taux courant sous forme de JOIN)
1097 """
1098 for tauxchange in self.tauxchange:
1099 if tauxchange.implantation_id == self.implantation_id:
1100 return tauxchange
1101 return None
1102
1103 def __unicode__(self):
1104 return u'%s %s (%s)' % (self.valeur, self.devise, self.annee)
1105
1106
1107 class ValeurPoint(ValeurPoint_):
1108 __doc__ = ValeurPoint_.__doc__
1109
1110
1111 class Devise(AUFMetadata):
1112 """Devise monétaire.
1113 """
1114 code = models.CharField(max_length=10, unique=True)
1115 nom = models.CharField(max_length=255)
1116
1117 class Meta:
1118 ordering = ['code']
1119 verbose_name = u"Devise"
1120 verbose_name_plural = u"Devises"
1121
1122 def __unicode__(self):
1123 return u'%s - %s' % (self.code, self.nom)
1124
1125 class TypeContrat(AUFMetadata):
1126 """Type de contrat.
1127 """
1128 nom = models.CharField(max_length=255)
1129 nom_long = models.CharField(max_length=255)
1130
1131 class Meta:
1132 ordering = ['nom']
1133 verbose_name = u"Type de contrat"
1134 verbose_name_plural = u"Types de contrat"
1135
1136 def __unicode__(self):
1137 return u'%s' % (self.nom)
1138
1139
1140 ### AUTRES
1141
1142 class ResponsableImplantation(AUFMetadata):
1143 """Le responsable d'une implantation.
1144 Anciennement géré sur le Dossier du responsable.
1145 """
1146 employe = models.ForeignKey('Employe', db_column='employe',
1147 related_name='+',
1148 null=True, blank=True)
1149 implantation = models.ForeignKey(ref.Implantation,
1150 db_column='implantation', related_name='+',
1151 unique=True)
1152
1153 def __unicode__(self):
1154 return u'%s : %s' % (self.implantation, self.employe)
1155
1156 class Meta:
1157 ordering = ['implantation__nom']
1158 verbose_name = "Responsable d'implantation"
1159 verbose_name_plural = "Responsables d'implantation"