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