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