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