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