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