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