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