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