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