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