link & icons
[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 REGIME_TRAVAIL_DEFAULT = 100.00
16 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = 35.00
17
18
19 # Upload de fichiers
20 storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
21 base_url=settings.PRIVE_MEDIA_URL)
22
23 def poste_piece_dispatch(instance, filename):
24 path = "poste/%s/%s" % (instance.poste_id, filename)
25 return path
26
27 def dossier_piece_dispatch(instance, filename):
28 path = "dossier/%s/%s" % (instance.dossier_id, filename)
29 return path
30
31 def employe_piece_dispatch(instance, filename):
32 path = "employe/%s/%s" % (instance.employe_id, filename)
33 return path
34
35
36 class Commentaire(AUFMetadata):
37 texte = models.TextField()
38 owner = models.ForeignKey('auth.User', db_column='owner', related_name='+')
39
40 class Meta:
41 abstract = True
42 ordering = ['-date_creation']
43
44 def __unicode__(self):
45 return u'%s' % (self.texte)
46
47
48 ### POSTE
49
50 POSTE_APPEL_CHOICES = (
51 ('interne', 'Interne'),
52 ('externe', 'Externe'),
53 )
54
55 class PosteManager(NoDeleteManager):
56 def get_query_set(self):
57 return super(PosteManager, self).get_query_set().select_related('implantation')
58
59 class Poste_(AUFMetadata):
60 """Un Poste est un emploi (job) à combler dans une implantation.
61 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
62 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
63 """
64
65 objects = PosteManager()
66
67 # Identification
68 nom = models.CharField(max_length=255,
69 verbose_name = u"Titre du poste", )
70 nom_feminin = models.CharField(max_length=255,
71 verbose_name = u"Titre du poste (au féminin)",
72 null=True)
73 implantation = models.ForeignKey(ref.Implantation,
74 db_column='implantation', related_name='+')
75 type_poste = models.ForeignKey('TypePoste', db_column='type_poste',
76 related_name='+',
77 null=True)
78 service = models.ForeignKey('Service', db_column='service', null=True,
79 related_name='+',
80 verbose_name = u"Direction/Service/Pôle support",
81 default=1) # default = Rectorat
82 responsable = models.ForeignKey('Poste', db_column='responsable',
83 related_name='+', null=True,
84 verbose_name = u"Poste du responsable",
85 default=149) # default = Recteur
86
87 # Contrat
88 regime_travail = models.DecimalField(max_digits=12, decimal_places=2,
89 default=REGIME_TRAVAIL_DEFAULT, null=True,
90 verbose_name = u"Temps de travail",
91 help_text="% du temps complet")
92 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
93 decimal_places=2, null=True,
94 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
95 verbose_name = u"Nb. heures par semaine")
96
97 # Recrutement
98 local = models.NullBooleanField(verbose_name = u"Local", default=True,
99 null=True, blank=True)
100 expatrie = models.NullBooleanField(verbose_name = u"Expatrié", default=False,
101 null=True, blank=True)
102 mise_a_disposition = models.NullBooleanField(
103 verbose_name = u"Mise à disposition",
104 null=True, default=False)
105 appel = models.CharField(max_length=10, null=True,
106 verbose_name = u"Appel à candidature",
107 choices=POSTE_APPEL_CHOICES,
108 default='interne')
109
110 # Rémunération
111 classement_min = models.ForeignKey('Classement',
112 db_column='classement_min', related_name='+',
113 null=True, blank=True)
114 classement_max = models.ForeignKey('Classement',
115 db_column='classement_max', related_name='+',
116 null=True, blank=True)
117 valeur_point_min = models.ForeignKey('ValeurPoint',
118 db_column='valeur_point_min', related_name='+',
119 null=True, blank=True)
120 valeur_point_max = models.ForeignKey('ValeurPoint',
121 db_column='valeur_point_max', related_name='+',
122 null=True, blank=True)
123 devise_min = models.ForeignKey('Devise', db_column='devise_min', null=True,
124 related_name='+', default=5)
125 devise_max = models.ForeignKey('Devise', db_column='devise_max', null=True,
126 related_name='+', default=5)
127 salaire_min = models.DecimalField(max_digits=12, decimal_places=2,
128 null=True, default=0)
129 salaire_max = models.DecimalField(max_digits=12, decimal_places=2,
130 null=True, default=0)
131 indemn_min = models.DecimalField(max_digits=12, decimal_places=2,
132 null=True, default=0)
133 indemn_max = models.DecimalField(max_digits=12, decimal_places=2,
134 null=True, default=0)
135 autre_min = models.DecimalField(max_digits=12, decimal_places=2,
136 null=True, default=0)
137 autre_max = models.DecimalField(max_digits=12, decimal_places=2,
138 null=True, default=0)
139
140 # Comparatifs de rémunération
141 devise_comparaison = models.ForeignKey('Devise', null=True,
142 db_column='devise_comparaison',
143 related_name='+',
144 default=5)
145 comp_locale_min = models.DecimalField(max_digits=12, decimal_places=2,
146 null=True, blank=True)
147 comp_locale_max = models.DecimalField(max_digits=12, decimal_places=2,
148 null=True, blank=True)
149 comp_universite_min = models.DecimalField(max_digits=12, decimal_places=2,
150 null=True, blank=True)
151 comp_universite_max = models.DecimalField(max_digits=12, decimal_places=2,
152 null=True, blank=True)
153 comp_fonctionpub_min = models.DecimalField(max_digits=12, decimal_places=2,
154 null=True, blank=True)
155 comp_fonctionpub_max = models.DecimalField(max_digits=12, decimal_places=2,
156 null=True, blank=True)
157 comp_ong_min = models.DecimalField(max_digits=12, decimal_places=2,
158 null=True, blank=True)
159 comp_ong_max = models.DecimalField(max_digits=12, decimal_places=2,
160 null=True, blank=True)
161 comp_autre_min = models.DecimalField(max_digits=12, decimal_places=2,
162 null=True, blank=True)
163 comp_autre_max = models.DecimalField(max_digits=12, decimal_places=2,
164 null=True, blank=True)
165
166 # Justification
167 justification = models.TextField(null=True, blank=True)
168
169 # Autres Metadata
170 date_validation = models.DateTimeField(null=True, blank=True) # de dae
171 date_debut = models.DateField(verbose_name=u"Date de début",
172 null=True, blank=True)
173 date_fin = models.DateField(verbose_name=u"Date de fin",
174 null=True, blank=True)
175
176 class Meta:
177 abstract = True
178 ordering = ['implantation__nom', 'nom']
179 verbose_name = u"Poste"
180 verbose_name_plural = u"Postes"
181
182 def __unicode__(self):
183 representation = u'%s - %s [%s]' % (self.implantation, self.nom,
184 self.id)
185 if self.is_vacant():
186 representation = representation + u' (VACANT)'
187 return representation
188
189 def is_vacant(self):
190 vacant = True
191 if self.occupe_par():
192 vacant = False
193 return vacant
194
195 def occupe_par(self):
196 """Retourne la liste d'employé occupant ce poste.
197 Généralement, retourne une liste d'un élément.
198 Si poste inoccupé, retourne liste vide.
199 """
200 return [d.employe for d in self.dossiers.filter(actif=True, supprime=False) \
201 .exclude(date_fin__lt=date.today())]
202
203 prefix_implantation = "implantation__region"
204 def get_regions(self):
205 return [self.implantation.region]
206
207
208 class Poste(Poste_):
209 __doc__ = Poste_.__doc__
210
211
212 class Poste(Poste_):
213 __doc__ = Poste_.__doc__
214
215
216 POSTE_FINANCEMENT_CHOICES = (
217 ('A', 'A - Frais de personnel'),
218 ('B', 'B - Projet(s)-Titre(s)'),
219 ('C', 'C - Autre')
220 )
221
222
223 class PosteFinancement_(models.Model):
224 """Pour un Poste, structure d'informations décrivant comment on prévoit
225 financer ce Poste.
226 """
227 poste = models.ForeignKey('Poste', db_column='poste',
228 related_name='%(app_label)s_financements')
229 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
230 pourcentage = models.DecimalField(max_digits=12, decimal_places=2,
231 help_text="ex.: 33.33 % (décimale avec point)")
232 commentaire = models.TextField(
233 help_text="Spécifiez la source de financement.")
234
235 class Meta:
236 abstract = True
237 ordering = ['type']
238
239 def __unicode__(self):
240 return u'%s : %s %' % (self.type, self.pourcentage)
241
242
243 class PosteFinancement(PosteFinancement_):
244 __doc__ = PosteFinancement_.__doc__
245
246
247 class PostePiece(models.Model):
248 """Documents relatifs au Poste.
249 Ex.: Description de poste
250 """
251 poste = models.ForeignKey('Poste', db_column='poste',
252 related_name='pieces')
253 nom = models.CharField(verbose_name = u"Nom", max_length=255)
254 fichier = models.FileField(verbose_name = u"Fichier",
255 upload_to=poste_piece_dispatch,
256 storage=storage_prive)
257
258 class Meta:
259 ordering = ['nom']
260
261 def __unicode__(self):
262 return u'%s' % (self.nom)
263
264 class PosteComparaison(models.Model):
265 """
266 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
267 """
268 poste = models.ForeignKey('Poste', related_name='comparaisons_internes')
269 implantation = models.ForeignKey(ref.Implantation, null=True, blank=True, related_name="+")
270 nom = models.CharField(verbose_name = u"Poste", max_length=255, null=True, blank=True)
271 montant = models.IntegerField(null=True)
272 devise = models.ForeignKey("Devise", default=5, related_name='+', null=True, blank=True)
273
274 def taux_devise(self):
275 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.implantation)
276 if len(liste_taux) == 0:
277 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.implantation))
278 else:
279 return liste_taux[0].taux
280
281 def montant_euros(self):
282 return round(float(self.montant) * float(self.taux_devise()), 2)
283
284
285 class PosteCommentaire(Commentaire):
286 poste = models.ForeignKey('Poste', db_column='poste', related_name='+')
287
288
289 ### EMPLOYÉ/PERSONNE
290
291 GENRE_CHOICES = (
292 ('M', 'Homme'),
293 ('F', 'Femme'),
294 )
295 SITUATION_CHOICES = (
296 ('C', 'Célibataire'),
297 ('F', 'Fiancé'),
298 ('M', 'Marié'),
299 )
300
301 class Employe(AUFMetadata):
302 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
303 Dossiers qu'il occupe ou a occupé de Postes.
304
305 Cette classe aurait pu avantageusement s'appeler Personne car la notion
306 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
307 """
308 # Identification
309 nom = models.CharField(max_length=255)
310 prenom = models.CharField(max_length=255, verbose_name = u"Prénom")
311 nom_affichage = models.CharField(max_length=255,
312 verbose_name = u"Nom d'affichage",
313 null=True, blank=True)
314 nationalite = models.ForeignKey(ref.Pays, to_field='code',
315 db_column='nationalite',
316 related_name='employes_nationalite',
317 verbose_name = u"Nationalité")
318 date_naissance = models.DateField(verbose_name = u"Date de naissance",
319 validators=[validate_date_passee],
320 null=True, blank=True)
321 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
322
323 # Infos personnelles
324 situation_famille = models.CharField(max_length=1,
325 choices=SITUATION_CHOICES,
326 verbose_name = u"Situation familiale",
327 null=True, blank=True)
328 date_entree = models.DateField(verbose_name = u"Date d'entrée à l'AUF",
329 null=True, blank=True)
330
331 # Coordonnées
332 tel_domicile = models.CharField(max_length=255,
333 verbose_name = u"Tél. domicile",
334 null=True, blank=True)
335 tel_cellulaire = models.CharField(max_length=255,
336 verbose_name = u"Tél. cellulaire",
337 null=True, blank=True)
338 adresse = models.CharField(max_length=255, null=True, blank=True)
339 ville = models.CharField(max_length=255, null=True, blank=True)
340 province = models.CharField(max_length=255, null=True, blank=True)
341 code_postal = models.CharField(max_length=255, null=True, blank=True)
342 pays = models.ForeignKey(ref.Pays, to_field='code', db_column='pays',
343 related_name='employes',
344 null=True, blank=True)
345
346 class Meta:
347 ordering = ['nom_affichage','nom','prenom']
348 verbose_name = u"Employé"
349 verbose_name_plural = u"Employés"
350
351 def __unicode__(self):
352 return u'%s [%s]' % (self.get_nom(), self.id)
353
354 def get_nom(self):
355 nom_affichage = self.nom_affichage
356 if not nom_affichage:
357 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
358 return nom_affichage
359
360 def civilite(self):
361 civilite = u''
362 if self.genre.upper() == u'M':
363 civilite = u'M.'
364 elif self.genre.upper() == u'F':
365 civilite = u'Mme'
366 return civilite
367
368 def url_photo(self):
369 """Retourne l'URL du service retournant la photo de l'Employe.
370 Équivalent reverse url 'rh_photo' avec id en param.
371 """
372 from django.core.urlresolvers import reverse
373 return reverse('rh_photo', kwargs={'id':self.id})
374
375 def dossiers_passes(self):
376 today = date.today()
377 dossiers_passes = self.dossiers.filter(date_fin__lt=today).order_by('-date_fin')
378 for d in dossiers_passes:
379 d.archive = True
380 return dossiers_passes
381
382 def dossiers_futurs(self):
383 today = date.today()
384 return self.dossiers.filter(date_debut__gt=today).order_by('-date_fin')
385
386 def dossiers_encours(self):
387 dossiers_p_f = self.dossiers_passes() | self.dossiers_futurs()
388 ids_dossiers_p_f = [d.id for d in dossiers_p_f]
389 dossiers_encours = self.dossiers.exclude(id__in=ids_dossiers_p_f).order_by('-date_fin')
390
391 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
392 for d in dossiers_encours:
393 d.remunerations = Remuneration.objects.filter(dossier=d.id).order_by('-id')
394 return dossiers_encours
395
396 def postes_encours(self):
397 postes_encours = set()
398 for d in self.dossiers_encours():
399 postes_encours.add(d.poste)
400 return postes_encours
401
402 def poste_principal(self):
403 """
404 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
405 Idée derrière :
406 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
407 """
408 poste = Poste.objects.none()
409 try:
410 poste = self.dossiers_encours().order_by('date_debut')[0].poste
411 except:
412 pass
413 return poste
414
415 prefix_implantation = "dossiers__poste__implantation__region"
416 def get_regions(self):
417 regions = []
418 for d in self.dossiers.all():
419 regions.append(d.poste.implantation.region)
420 return regions
421
422
423 class EmployePiece(models.Model):
424 """Documents relatifs à un employé.
425 Ex.: CV...
426 """
427 employe = models.ForeignKey('Employe', db_column='employe')
428 nom = models.CharField(verbose_name="Nom", max_length=255)
429 fichier = models.FileField(verbose_name="Fichier",
430 upload_to=employe_piece_dispatch,
431 storage=storage_prive)
432
433 class Meta:
434 ordering = ['nom']
435 verbose_name = u"Employé pièce"
436 verbose_name_plural = u"Employé pièces"
437
438 def __unicode__(self):
439 return u'%s' % (self.nom)
440
441 class EmployeCommentaire(Commentaire):
442 employe = models.ForeignKey('Employe', db_column='employe',
443 related_name='+')
444
445 class Meta:
446 verbose_name = u"Employé commentaire"
447 verbose_name_plural = u"Employé commentaires"
448
449
450 LIEN_PARENTE_CHOICES = (
451 ('Conjoint', 'Conjoint'),
452 ('Conjointe', 'Conjointe'),
453 ('Fille', 'Fille'),
454 ('Fils', 'Fils'),
455 )
456
457 class AyantDroit(AUFMetadata):
458 """Personne en relation avec un Employe.
459 """
460 # Identification
461 nom = models.CharField(max_length=255)
462 prenom = models.CharField(max_length=255,
463 verbose_name = u"Prénom",)
464 nom_affichage = models.CharField(max_length=255,
465 verbose_name = u"Nom d'affichage",
466 null=True, blank=True)
467 nationalite = models.ForeignKey(ref.Pays, to_field='code',
468 db_column='nationalite',
469 related_name='ayantdroits_nationalite',
470 verbose_name = u"Nationalité")
471 date_naissance = models.DateField(verbose_name = u"Date de naissance",
472 validators=[validate_date_passee],
473 null=True, blank=True)
474 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
475
476 # Relation
477 employe = models.ForeignKey('Employe', db_column='employe',
478 related_name='ayantdroits',
479 verbose_name = u"Employé")
480 lien_parente = models.CharField(max_length=10,
481 choices=LIEN_PARENTE_CHOICES,
482 verbose_name = u"Lien de parenté",
483 null=True, blank=True)
484
485 class Meta:
486 ordering = ['nom_affichage']
487 verbose_name = u"Ayant droit"
488 verbose_name_plural = u"Ayants droit"
489
490 def __unicode__(self):
491 return u'%s' % (self.get_nom())
492
493 def get_nom(self):
494 nom_affichage = self.nom_affichage
495 if not nom_affichage:
496 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
497 return nom_affichage
498
499 prefix_implantation = "employe__dossiers__poste__implantation__region"
500 def get_regions(self):
501 regions = []
502 for d in self.employe.dossiers.all():
503 regions.append(d.poste.implantation.region)
504 return regions
505
506
507 class AyantDroitCommentaire(Commentaire):
508 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
509 related_name='+')
510
511
512 ### DOSSIER
513
514 STATUT_RESIDENCE_CHOICES = (
515 ('local', 'Local'),
516 ('expat', 'Expatrié'),
517 )
518
519 COMPTE_COMPTA_CHOICES = (
520 ('coda', 'CODA'),
521 ('scs', 'SCS'),
522 ('aucun', 'Aucun'),
523 )
524
525 class Dossier_(AUFMetadata):
526 """Le Dossier regroupe les informations relatives à l'occupation
527 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
528 par un Employe.
529
530 Plusieurs Contrats peuvent être associés au Dossier.
531 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
532 lequel aucun Dossier n'existe est un poste vacant.
533 """
534 # Identification
535 employe = models.ForeignKey('Employe', db_column='employe',
536 related_name='dossiers',
537 verbose_name=u"Employé")
538 # TODO: OneToOne ??
539 poste = models.ForeignKey('Poste', db_column='poste', related_name='dossiers')
540 statut = models.ForeignKey('Statut', related_name='+', default=3,
541 null=True)
542 organisme_bstg = models.ForeignKey('OrganismeBstg',
543 db_column='organisme_bstg',
544 related_name='+',
545 verbose_name = u"Organisme",
546 help_text="Si détaché (DET) ou \
547 mis à disposition (MAD), \
548 préciser l'organisme.",
549 null=True, blank=True)
550
551 # Recrutement
552 remplacement = models.BooleanField(default=False)
553 remplacement_de = models.ForeignKey('self', related_name='+',
554 null=True, blank=True)
555 statut_residence = models.CharField(max_length=10, default='local',
556 verbose_name = u"Statut", null=True,
557 choices=STATUT_RESIDENCE_CHOICES)
558
559 # Rémunération
560 classement = models.ForeignKey('Classement', db_column='classement',
561 related_name='+',
562 null=True, blank=True)
563 regime_travail = models.DecimalField(max_digits=12, null=True,
564 decimal_places=2,
565 default=REGIME_TRAVAIL_DEFAULT,
566 verbose_name = u"Régime de travail",
567 help_text="% du temps complet")
568 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
569 decimal_places=2, null=True,
570 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
571 verbose_name = u"Nb. heures par semaine")
572
573 # Occupation du Poste par cet Employe (anciennement "mandat")
574 date_debut = models.DateField(verbose_name = u"Date de début d'occupation \
575 de poste",)
576 date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \
577 de poste",
578 null=True, blank=True)
579
580 # Comptes
581 # TODO?
582
583 class Meta:
584 abstract = True
585 ordering = ['employe__nom', ]
586 verbose_name = u"Dossier"
587 verbose_name_plural = "Dossiers"
588
589 def salaire_theorique(self):
590 annee = date.today().year
591 coeff = self.classement.coefficient
592 implantation = self.poste.implantation
593 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
594
595 montant = coeff * point.valeur
596 devise = point.devise
597 return {'montant':montant, 'devise':devise}
598
599 def __unicode__(self):
600 poste = self.poste.nom
601 if self.employe.genre == 'F':
602 poste = self.poste.nom_feminin
603 return u'%s - %s' % (self.employe, poste)
604
605 prefix_implantation = "poste__implantation__region"
606 def get_regions(self):
607 return [self.poste.implantation.region]
608
609
610 def remunerations(self):
611 return self.rh_remuneration_remunerations.all().order_by('date_debut')
612
613
614 class Dossier(Dossier_):
615 __doc__ = Dossier_.__doc__
616
617
618 class DossierPiece(models.Model):
619 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
620 Ex.: Lettre de motivation.
621 """
622 dossier = models.ForeignKey('Dossier', db_column='dossier',
623 related_name='+')
624 nom = models.CharField(verbose_name = u"Nom", max_length=255)
625 fichier = models.FileField(verbose_name = u"Fichier",
626 upload_to=dossier_piece_dispatch,
627 storage=storage_prive)
628
629 class Meta:
630 ordering = ['nom']
631
632 def __unicode__(self):
633 return u'%s' % (self.nom)
634
635 class DossierCommentaire(Commentaire):
636 dossier = models.ForeignKey('Dossier', db_column='dossier',
637 related_name='+')
638
639 class DossierComparaison(models.Model):
640 """
641 Photo d'une comparaison salariale au moment de l'embauche.
642 """
643 dossier = models.ForeignKey('Dossier', related_name='comparaisons')
644 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
645 poste = models.CharField(max_length=255, null=True, blank=True)
646 personne = models.CharField(max_length=255, null=True, blank=True)
647 montant = models.IntegerField(null=True)
648 devise = models.ForeignKey('Devise', default=5, related_name='+', null=True, blank=True)
649
650 def taux_devise(self):
651 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.dossier.poste.implantation)
652 if len(liste_taux) == 0:
653 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.dossier.poste.implantation))
654 else:
655 return liste_taux[0].taux
656
657 def montant_euros(self):
658 return round(float(self.montant) * float(self.taux_devise()), 2)
659
660
661 ### RÉMUNÉRATION
662
663 class RemunerationMixin(AUFMetadata):
664 # Identification
665 dossier = models.ForeignKey('Dossier', db_column='dossier',
666 related_name='%(app_label)s_%(class)s_remunerations')
667 type = models.ForeignKey('TypeRemuneration', db_column='type',
668 related_name='+',
669 verbose_name = u"Type de rémunération")
670 type_revalorisation = models.ForeignKey('TypeRevalorisation',
671 db_column='type_revalorisation',
672 related_name='+',
673 verbose_name = u"Type de revalorisation",
674 null=True, blank=True)
675 montant = models.FloatField(null=True, blank=True,
676 default=0)
677 # Annuel (12 mois, 52 semaines, 364 jours?)
678 devise = models.ForeignKey('Devise', to_field='id',
679 db_column='devise', related_name='+',
680 default=5)
681 # commentaire = precision
682 commentaire = models.CharField(max_length=255, null=True, blank=True)
683 # date_debut = anciennement date_effectif
684 date_debut = models.DateField(verbose_name = u"Date de début",
685 null=True, blank=True)
686 date_fin = models.DateField(verbose_name = u"Date de fin",
687 null=True, blank=True)
688
689 class Meta:
690 abstract = True
691 ordering = ['type__nom', '-date_fin']
692
693 def __unicode__(self):
694 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
695
696 class Remuneration_(RemunerationMixin):
697 """Structure de rémunération (données budgétaires) en situation normale
698 pour un Dossier. Si un Evenement existe, utiliser la structure de
699 rémunération EvenementRemuneration de cet événement.
700 """
701
702 def montant_mois(self):
703 return round(self.montant / 12, 2)
704
705 def taux_devise(self):
706 return self.devise.tauxchange_set.order_by('-annee').all()[0].taux
707
708 def montant_euro(self):
709 return round(float(self.montant) / float(self.taux_devise()), 2)
710
711 def montant_euro_mois(self):
712 return round(self.montant_euro() / 12, 2)
713
714 def __unicode__(self):
715 try:
716 devise = self.devise.code
717 except:
718 devise = "???"
719 return "%s %s" % (self.montant, devise)
720
721 class Meta:
722 abstract = True
723 verbose_name = u"Rémunération"
724 verbose_name_plural = u"Rémunérations"
725
726
727 class Remuneration(Remuneration_):
728 __doc__ = Remuneration_.__doc__
729
730
731 ### CONTRATS
732
733 class ContratManager(NoDeleteManager):
734 def get_query_set(self):
735 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
736
737
738 class Contrat(AUFMetadata):
739 """Document juridique qui encadre la relation de travail d'un Employe
740 pour un Poste particulier. Pour un Dossier (qui documente cette
741 relation de travail) plusieurs contrats peuvent être associés.
742 """
743
744 objects = ContratManager()
745
746 dossier = models.ForeignKey('Dossier', db_column='dossier',
747 related_name='contrats')
748 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
749 related_name='+',
750 verbose_name = u"Type de contrat")
751 date_debut = models.DateField(verbose_name = u"Date de début")
752 date_fin = models.DateField(verbose_name = u"Date de fin",
753 null=True, blank=True)
754
755 class Meta:
756 ordering = ['dossier__employe__nom_affichage']
757 verbose_name = u"Contrat"
758 verbose_name_plural = u"Contrats"
759
760 def __unicode__(self):
761 return u'%s - %s' % (self.dossier, self.id)
762
763 # TODO? class ContratPiece(models.Model):
764
765
766 ### ÉVÉNEMENTS
767
768 class Evenement_(AUFMetadata):
769 """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
770 d'un Dossier qui vient altérer des informations normales liées à un Dossier
771 (ex.: la Remuneration).
772
773 Ex.: congé de maternité, maladie...
774
775 Lors de ces situations exceptionnelles, l'Employe a un régime de travail
776 différent et une rémunération en conséquence. On souhaite toutefois
777 conserver le Dossier intact afin d'éviter une re-saisie des données lors
778 du retour à la normale.
779 """
780 dossier = models.ForeignKey('Dossier', db_column='dossier',
781 related_name='+')
782 nom = models.CharField(max_length=255)
783 date_debut = models.DateField(verbose_name = u"Date de début")
784 date_fin = models.DateField(verbose_name = u"Date de fin",
785 null=True, blank=True)
786
787 class Meta:
788 abstract = True
789 ordering = ['nom']
790 verbose_name = u"Évènement"
791 verbose_name_plural = u"Évènements"
792
793 def __unicode__(self):
794 return u'%s' % (self.nom)
795
796
797 class Evenement(Evenement_):
798 __doc__ = Evenement_.__doc__
799
800
801 class EvenementRemuneration_(RemunerationMixin):
802 """Structure de rémunération liée à un Evenement qui remplace
803 temporairement la Remuneration normale d'un Dossier, pour toute la durée
804 de l'Evenement.
805 """
806 evenement = models.ForeignKey("Evenement", db_column='evenement',
807 related_name='+',
808 verbose_name = u"Évènement")
809 # TODO : le champ dossier hérité de Remuneration doit être dérivé
810 # de l'Evenement associé
811
812 class Meta:
813 abstract = True
814 ordering = ['evenement', 'type__nom', '-date_fin']
815 verbose_name = u"Évènement - rémunération"
816 verbose_name_plural = u"Évènements - rémunérations"
817
818
819 class EvenementRemuneration(EvenementRemuneration_):
820 __doc__ = EvenementRemuneration_.__doc__
821
822 class Meta:
823 abstract = True
824
825
826 class EvenementRemuneration(EvenementRemuneration_):
827 __doc__ = EvenementRemuneration_.__doc__
828
829
830 ### RÉFÉRENCES RH
831
832 class FamilleEmploi(AUFMetadata):
833 """Catégorie utilisée dans la gestion des Postes.
834 Catégorie supérieure à TypePoste.
835 """
836 nom = models.CharField(max_length=255)
837
838 class Meta:
839 ordering = ['nom']
840 verbose_name = u"Famille d'emploi"
841 verbose_name_plural = u"Familles d'emploi"
842
843 def __unicode__(self):
844 return u'%s' % (self.nom)
845
846 class TypePoste(AUFMetadata):
847 """Catégorie de Poste.
848 """
849 nom = models.CharField(max_length=255)
850 nom_feminin = models.CharField(max_length=255,
851 verbose_name = u"Nom féminin")
852
853 is_responsable = models.BooleanField(default=False,
854 verbose_name = u"Poste de responsabilité")
855 famille_emploi = models.ForeignKey('FamilleEmploi',
856 db_column='famille_emploi',
857 related_name='+',
858 verbose_name = u"Famille d'emploi")
859
860 class Meta:
861 ordering = ['nom']
862 verbose_name = u"Type de poste"
863 verbose_name_plural = u"Types de poste"
864
865 def __unicode__(self):
866 return u'%s' % (self.nom)
867
868
869 TYPE_PAIEMENT_CHOICES = (
870 ('Régulier', 'Régulier'),
871 ('Ponctuel', 'Ponctuel'),
872 )
873
874 NATURE_REMUNERATION_CHOICES = (
875 ('Accessoire', 'Accessoire'),
876 ('Charges', 'Charges'),
877 ('Indemnité', 'Indemnité'),
878 ('RAS', 'Rémunération autre source'),
879 ('Traitement', 'Traitement'),
880 )
881
882 class TypeRemuneration(AUFMetadata):
883 """Catégorie de Remuneration.
884 """
885 nom = models.CharField(max_length=255)
886 type_paiement = models.CharField(max_length=30,
887 choices=TYPE_PAIEMENT_CHOICES,
888 verbose_name = u"Type de paiement")
889 nature_remuneration = models.CharField(max_length=30,
890 choices=NATURE_REMUNERATION_CHOICES,
891 verbose_name = u"Nature de la rémunération")
892
893 class Meta:
894 ordering = ['nom']
895 verbose_name = u"Type de rémunération"
896 verbose_name_plural = u"Types de rémunération"
897
898 def __unicode__(self):
899 return u'%s' % (self.nom)
900
901 class TypeRevalorisation(AUFMetadata):
902 """Justification du changement de la Remuneration.
903 (Actuellement utilisé dans aucun traitement informatique.)
904 """
905 nom = models.CharField(max_length=255)
906
907 class Meta:
908 ordering = ['nom']
909 verbose_name = u"Type de revalorisation"
910 verbose_name_plural = u"Types de revalorisation"
911
912 def __unicode__(self):
913 return u'%s' % (self.nom)
914
915 class Service(AUFMetadata):
916 """Unité administrative où les Postes sont rattachés.
917 """
918 nom = models.CharField(max_length=255)
919
920 class Meta:
921 ordering = ['nom']
922 verbose_name = u"Service"
923 verbose_name_plural = u"Services"
924
925 def __unicode__(self):
926 return u'%s' % (self.nom)
927
928
929 TYPE_ORGANISME_CHOICES = (
930 ('MAD', 'Mise à disposition'),
931 ('DET', 'Détachement'),
932 )
933
934 class OrganismeBstg(AUFMetadata):
935 """Organisation d'où provient un Employe mis à disposition (MAD) de
936 ou détaché (DET) à l'AUF à titre gratuit.
937
938 (BSTG = bien et service à titre gratuit.)
939 """
940 nom = models.CharField(max_length=255)
941 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
942 pays = models.ForeignKey(ref.Pays, to_field='code',
943 db_column='pays',
944 related_name='organismes_bstg',
945 null=True, blank=True)
946
947 class Meta:
948 ordering = ['type', 'nom']
949 verbose_name = u"Organisme BSTG"
950 verbose_name_plural = u"Organismes BSTG"
951
952 def __unicode__(self):
953 return u'%s (%s)' % (self.nom, self.get_type_display())
954
955 prefix_implantation = "pays__region"
956 def get_regions(self):
957 return [self.pays.region]
958
959
960 class Statut(AUFMetadata):
961 """Statut de l'Employe dans le cadre d'un Dossier particulier.
962 """
963 # Identification
964 code = models.CharField(max_length=25, unique=True, help_text="Saisir un code court mais lisible pour ce statut : le code est utilisé pour associer les statuts aux autres données tout en demeurant plus lisible qu'un identifiant numérique.")
965 nom = models.CharField(max_length=255)
966
967 class Meta:
968 ordering = ['code']
969 verbose_name = u"Statut d'employé"
970 verbose_name_plural = u"Statuts d'employé"
971
972 def __unicode__(self):
973 return u'%s : %s' % (self.code, self.nom)
974
975
976 TYPE_CLASSEMENT_CHOICES = (
977 ('S', 'S -Soutien'),
978 ('T', 'T - Technicien'),
979 ('P', 'P - Professionel'),
980 ('C', 'C - Cadre'),
981 ('D', 'D - Direction'),
982 ('SO', 'SO - Sans objet [expatriés]'),
983 ('HG', 'HG - Hors grille [direction]'),
984 )
985
986
987 class Classement_(AUFMetadata):
988 """Éléments de classement de la
989 "Grille générique de classement hiérarchique".
990
991 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
992 classement dans la grille. Le classement donne le coefficient utilisé dans:
993
994 salaire de base = coefficient * valeur du point de l'Implantation du Poste
995 """
996 # Identification
997 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
998 echelon = models.IntegerField(verbose_name = u"Échelon")
999 degre = models.IntegerField(verbose_name = u"Degré")
1000 coefficient = models.FloatField(default=0, verbose_name = u"Coéfficient",
1001 null=True)
1002 # Méta
1003 # annee # au lieu de date_debut et date_fin
1004 commentaire = models.TextField(null=True, blank=True)
1005
1006 class Meta:
1007 abstract = True
1008 ordering = ['type','echelon','degre','coefficient']
1009 verbose_name = u"Classement"
1010 verbose_name_plural = u"Classements"
1011
1012 def __unicode__(self):
1013 return u'%s.%s.%s (%s)' % (self.type, self.echelon, self.degre,
1014 self.coefficient)
1015
1016 class Classement(Classement_):
1017 __doc__ = Classement_.__doc__
1018
1019
1020 class TauxChange_(AUFMetadata):
1021 """Taux de change de la devise vers l'euro (EUR)
1022 pour chaque année budgétaire.
1023 """
1024 # Identification
1025 devise = models.ForeignKey('Devise', db_column='devise')
1026 annee = models.IntegerField(verbose_name = u"Année")
1027 taux = models.FloatField(verbose_name = u"Taux vers l'euro")
1028
1029 class Meta:
1030 abstract = True
1031 ordering = ['-annee', 'devise__code']
1032 verbose_name = u"Taux de change"
1033 verbose_name_plural = u"Taux de change"
1034
1035 def __unicode__(self):
1036 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1037
1038
1039 class TauxChange(TauxChange_):
1040 __doc__ = TauxChange_.__doc__
1041
1042 class ValeurPointManager(NoDeleteManager):
1043 def get_query_set(self):
1044 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1045
1046
1047 class ValeurPoint_(AUFMetadata):
1048 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1049 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1050 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1051
1052 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1053 """
1054
1055 objects = ValeurPointManager()
1056
1057 valeur = models.FloatField(null=True)
1058 devise = models.ForeignKey('Devise', db_column='devise', null=True,
1059 related_name='+', default=5)
1060 implantation = models.ForeignKey(ref.Implantation,
1061 db_column='implantation',
1062 related_name='%(app_label)s_valeur_point')
1063 # Méta
1064 annee = models.IntegerField()
1065
1066 class Meta:
1067 ordering = ['-annee', 'implantation__nom']
1068 abstract = True
1069 verbose_name = u"Valeur du point"
1070 verbose_name_plural = u"Valeurs du point"
1071
1072 # TODO : cette fonction n'était pas présente dans la branche dev, utilité?
1073 def get_tauxchange_courant(self):
1074 """
1075 Recherche le taux courant associé à la valeur d'un point.
1076 Tous les taux de l'année courante sont chargés, pour optimiser un
1077 affichage en liste. (On pourrait probablement améliorer le manager pour
1078 lui greffer le taux courant sous forme de JOIN)
1079 """
1080 for tauxchange in self.tauxchange:
1081 if tauxchange.implantation_id == self.implantation_id:
1082 return tauxchange
1083 return None
1084
1085 def __unicode__(self):
1086 return u'%s %s (%s)' % (self.valeur, self.devise, self.annee)
1087
1088
1089 class ValeurPoint(ValeurPoint_):
1090 __doc__ = ValeurPoint_.__doc__
1091
1092
1093 class Devise(AUFMetadata):
1094 """Devise monétaire.
1095 """
1096 code = models.CharField(max_length=10, unique=True)
1097 nom = models.CharField(max_length=255)
1098
1099 class Meta:
1100 ordering = ['code']
1101 verbose_name = u"Devise"
1102 verbose_name_plural = u"Devises"
1103
1104 def __unicode__(self):
1105 return u'%s - %s' % (self.code, self.nom)
1106
1107 class TypeContrat(AUFMetadata):
1108 """Type de contrat.
1109 """
1110 nom = models.CharField(max_length=255)
1111 nom_long = models.CharField(max_length=255)
1112
1113 class Meta:
1114 ordering = ['nom']
1115 verbose_name = u"Type de contrat"
1116 verbose_name_plural = u"Types de contrat"
1117
1118 def __unicode__(self):
1119 return u'%s' % (self.nom)
1120
1121
1122 ### AUTRES
1123
1124 class ResponsableImplantation(AUFMetadata):
1125 """Le responsable d'une implantation.
1126 Anciennement géré sur le Dossier du responsable.
1127 """
1128 employe = models.ForeignKey('Employe', db_column='employe',
1129 related_name='+',
1130 null=True, blank=True)
1131 implantation = models.ForeignKey(ref.Implantation,
1132 db_column='implantation', related_name='+',
1133 unique=True)
1134
1135 def __unicode__(self):
1136 return u'%s : %s' % (self.implantation, self.employe)
1137
1138 class Meta:
1139 ordering = ['implantation__nom']
1140 verbose_name = "Responsable d'implantation"
1141 verbose_name_plural = "Responsables d'implantation"