securite accès vues poste
[auf_rh_dae.git] / project / dae / models.py
1 # -=- encoding: utf-8 -=-
2
3 import os
4 from django.conf import settings
5 from django.core.files.storage import FileSystemStorage
6 from django.db import models
7 from django.db.models import Q
8 from django.contrib.auth.models import Group
9 import reversion
10 from workflow import PosteWorkflow, DossierWorkflow
11 import datamaster_modeles.models as ref
12 from rh_v1 import models as rh
13 from utils import is_user_dans_service, get_employe_from_user
14
15 # Groupes impliqués dans le Worflow
16 grp_administrateurs, created = Group.objects.get_or_create(name='Administrateurs')
17 grp_gestionnaires, created = Group.objects.get_or_create(name='Gestionnaires')
18 grp_directeurs_bureau, created = Group.objects.get_or_create(name='Directeurs de bureau')
19 grp_drh, created = Group.objects.get_or_create(name='DRH')
20 grp_pole_financier, created = Group.objects.get_or_create(name='Pôle financier')
21 grp_haute_direction, created = Group.objects.get_or_create(name='Haute direction')
22 grp_service_utilisateurs, created = Group.objects.get_or_create(name='Service utilisateurs')
23 grp_directeurs_service, created = Group.objects.get_or_create(name='Directeurs de service / pôle')
24 grp_correspondants_rh, created = Group.objects.get_or_create(name='Correspondants RH')
25
26 dae_groupes = (grp_administrateurs, grp_gestionnaires, grp_directeurs_bureau, grp_drh,
27 grp_pole_financier, grp_haute_direction, grp_service_utilisateurs,
28 grp_directeurs_service, grp_correspondants_rh, )
29
30 STATUT_RESIDENCE_CHOICES = (
31 ('local', 'Local'),
32 ('expat', 'Expatrié'),
33 )
34
35 POSTE_APPEL_CHOICES = (
36 ('interne', 'Interne'),
37 ('externe', 'Externe'),
38 )
39
40 POSTE_STATUT_CHOICES = (
41 ('MAD', 'Mise à disposition'),
42 ('DET', 'Détachement'),
43 )
44
45 # Upload de fichiers
46 storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT, base_url=settings.PRIVE_MEDIA_URL)
47
48 def poste_piece_dispatch(instance, filename):
49 path = "poste/%s/%s" % (instance.poste_id, filename)
50 return path
51
52 def dossier_piece_dispatch(instance, filename):
53 path = "dossier/%s/%s" % (instance.dossier_id, filename)
54 return path
55
56
57 class PostePiece(models.Model):
58 poste = models.ForeignKey("Poste")
59 nom = models.CharField(verbose_name="Nom", max_length=255)
60 fichier = models.FileField(verbose_name="Fichier", upload_to=poste_piece_dispatch, storage=storage_prive)
61
62 class PosteManager(models.Manager):
63 """
64 Chargement de tous les objets FK existants sur chaque QuerySet.
65 """
66 def get_query_set(self):
67 fkeys = (
68 'id_rh',
69 'responsable',
70 'implantation',
71 'type_poste',
72 'service',
73 'classement_min',
74 'classement_max',
75 'valeur_point_min',
76 'valeur_point_max',
77 )
78 return super(PosteManager, self).get_query_set() \
79 .select_related(*fkeys).all()
80
81 def ma_region_ou_service(self, user):
82 """
83 Filtrage des postes en fonction du user connecté (region / service)
84 """
85 # SERVICE
86 employe = get_employe_from_user(user)
87 if is_user_dans_service(user):
88 q = Q(implantation=employe.implantation)
89 # REGION
90 else:
91 q = Q(implantation__region=employe.implantation.region)
92 return self.get_query_set().filter(q)
93
94 class Poste(PosteWorkflow, models.Model):
95 # Modèle existant
96 id_rh = models.ForeignKey(rh.Poste, null=True, related_name='+',
97 editable=False,
98 verbose_name="Mise à jour du poste")
99 nom = models.CharField(verbose_name="Titre du poste", max_length=255)
100 implantation = models.ForeignKey(ref.Implantation)
101 type_poste = models.ForeignKey(rh.TypePoste, null=True, related_name='+')
102 service = models.ForeignKey(rh.Service, related_name='+',
103 verbose_name=u"Direction/Service/Pôle support")
104 responsable = models.ForeignKey(rh.Poste, related_name='+',
105 verbose_name="Poste du responsable")
106
107 regime_travail = models.DecimalField(max_digits=12, decimal_places=2,
108 default=100,
109 verbose_name="Temps de travail",
110 help_text="% du temps complet")
111 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
112 decimal_places=2,
113 default=35,
114 verbose_name="Nb. heures par semaine")
115
116 # Recrutement
117 local = models.BooleanField(verbose_name="Local", default=True, blank=True)
118 expatrie = models.BooleanField(verbose_name="Expatrié", default=False, blank=True)
119
120 # TODO null?
121 mise_a_disposition = models.BooleanField(verbose_name="Mise à disposition")
122 appel = models.CharField(max_length=10, default='interne',
123 verbose_name="Appel à candidature",
124 choices=POSTE_APPEL_CHOICES)
125
126 # Rémunération
127 classement_min = models.ForeignKey(rh.Classement, related_name='+')
128 classement_max = models.ForeignKey(rh.Classement, related_name='+')
129
130 # En fait, les coefficient n'ont pas de valeur dans ces cas, les couts sont calculés
131 # et mis dans les coûts globals
132 #coefficient_min = models.FloatField(null=True) # pour classement "hors grille"
133 #coefficient_max = models.FloatField(null=True) # pour classement "hors grille"
134
135 valeur_point_min = models.ForeignKey(rh.ValeurPoint, related_name='+', blank=True, null=True)
136 valeur_point_max = models.ForeignKey(rh.ValeurPoint, related_name='+', blank=True, null=True)
137 devise_min = models.ForeignKey(rh.Devise, default=5, related_name='+')
138 devise_max = models.ForeignKey(rh.Devise, default=5, related_name='+')
139 salaire_min = models.DecimalField(max_digits=12, decimal_places=2,
140 default=0)
141 salaire_max = models.DecimalField(max_digits=12, decimal_places=2,
142 default=0)
143 indemn_min = models.DecimalField(max_digits=12, decimal_places=2,
144 default=0)
145 indemn_max = models.DecimalField(max_digits=12, decimal_places=2,
146 default=0)
147 autre_min = models.DecimalField(max_digits=12, decimal_places=2,
148 default=0)
149 autre_max = models.DecimalField(max_digits=12, decimal_places=2,
150 default=0)
151
152 # Comparatifs de rémunération
153 devise_comparaison = models.ForeignKey(rh.Devise, related_name='+',
154 default=5)
155 comp_locale_min = models.DecimalField(max_digits=12, decimal_places=2,
156 null=True, blank=True)
157 comp_locale_max = models.DecimalField(max_digits=12, decimal_places=2,
158 null=True, blank=True)
159 comp_universite_min = models.DecimalField(max_digits=12, decimal_places=2,
160 null=True, blank=True)
161 comp_universite_max = models.DecimalField(max_digits=12, decimal_places=2,
162 null=True, blank=True)
163 comp_fonctionpub_min = models.DecimalField(max_digits=12, decimal_places=2,
164 null=True, blank=True)
165 comp_fonctionpub_max = models.DecimalField(max_digits=12, decimal_places=2,
166 null=True, blank=True)
167 comp_ong_min = models.DecimalField(max_digits=12, decimal_places=2,
168 null=True, blank=True)
169 comp_ong_max = models.DecimalField(max_digits=12, decimal_places=2,
170 null=True, blank=True)
171 comp_autre_min = models.DecimalField(max_digits=12, decimal_places=2,
172 null=True, blank=True)
173 comp_autre_max = models.DecimalField(max_digits=12, decimal_places=2,
174 null=True, blank=True)
175
176 # Justification
177 justification = models.TextField()
178
179 # Méta
180 date_creation = models.DateTimeField(auto_now_add=True)
181 date_modification = models.DateTimeField(auto_now=True)
182 date_debut = models.DateField(verbose_name="Date de début",
183 help_text="format: aaaa-mm-jj")
184 date_fin = models.DateField(null=True, blank=True,
185 verbose_name="Date de fin",
186 help_text="format: aaaa-mm-jj")
187 actif = models.BooleanField(default=True)
188
189 # Managers
190 objects = PosteManager()
191
192 def _get_key(self):
193 """
194 Les vues sont montées selon une clef spéciale pour identifier la provenance du poste.
195 Cette méthode fournit un moyen de reconstruire cette clef afin de générer les URLs.
196 """
197 return "dae-%s" % self.id
198 key = property(_get_key)
199
200 def get_dossiers(self):
201 """
202 Liste tous les anciens dossiers liés à ce poste.
203 (Le nom de la relation sur le rh.Poste est mal choisi poste1 au lieu de dossier1)
204 Note1 : seulement le dosssier principal fait l'objet de la recherche.
205 Note2 : les dossiers sont retournés du plus récent au plus vieux. (Ce test est fait
206 en fonction du id, car les dates de création sont absentes de rh v1).
207 """
208 if self.id_rh is None:
209 return []
210 postes = [p for p in self.id_rh.poste1.all()]
211 return sorted(postes, key=lambda poste: poste.id, reverse=True)
212
213 def get_complement_nom(self):
214 """
215 Inspecte les modèles rh v1 pour trouver dans le dernier dossier un complément de titre de poste.
216 """
217 dossiers = self.get_dossiers()
218 if len(dossiers) > 0:
219 nom = dossiers[0].complement1
220 else:
221 nom = ""
222 return nom
223
224 def get_employe(self):
225 """
226 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
227 """
228 dossiers = self.get_dossiers()
229 if len(dossiers) > 0:
230 return dossiers[0].employe
231 else:
232 return None
233
234 def get_default_devise(self):
235 """Récupère la devise par défaut en fonction de l'implantation (EUR autrement)"""
236 try:
237 implantation_devise = rh.TauxChange.objects.filter(implantation=self.implantation)[0].devise
238 except:
239 implantation_devise = 5 # EUR
240 return implantation_devise
241
242 #####################
243 # Classement de poste
244 #####################
245
246 def get_couts_minimum(self):
247 return (float)(self.salaire_min + self.indemn_min + self.autre_min)
248
249 def get_taux_minimum(self):
250 try:
251 return rh.TauxChange.objects.filter(implantation=self.implantation, devise=self.devise_min)[0].taux
252 except:
253 return 1
254
255 def get_couts_minimum_euros(self):
256 return self.get_couts_minimum() * self.get_taux_minimum()
257
258 def get_couts_maximum(self):
259 return (float)(self.salaire_max + self.indemn_max + self.autre_max)
260
261 def get_taux_maximum(self):
262 try:
263 return rh.TauxChange.objects.filter(implantation=self.implantation, devise=self.devise_max)[0].taux
264 except:
265 return 1
266
267 def get_couts_maximum_euros(self):
268 return self.get_couts_maximum() * self.get_taux_maximum()
269
270 ######################
271 # Comparaison de poste
272 ######################
273
274 def get_taux_comparaison(self):
275 try:
276 return rh.TauxChange.objects.filter(implantation=self.implantation, devise=self.devise_comparaison)[0].taux
277 except:
278 return 1
279
280 def get_comp_universite_min_euros(self):
281 return (float)(self.comp_universite_min) * self.get_taux_comparaison()
282
283 def get_comp_fonctionpub_min_euros(self):
284 return (float)(self.comp_fonctionpub_min) * self.get_taux_comparaison()
285
286 def get_comp_locale_min_euros(self):
287 return (float)(self.comp_locale_min) * self.get_taux_comparaison()
288
289 def get_comp_ong_min_euros(self):
290 return (float)(self.comp_ong_min) * self.get_taux_comparaison()
291
292 def get_comp_autre_min_euros(self):
293 return (float)(self.comp_autre_min) * self.get_taux_comparaison()
294
295 def get_comp_universite_max_euros(self):
296 return (float)(self.comp_universite_max) * self.get_taux_comparaison()
297
298 def get_comp_fonctionpub_max_euros(self):
299 return (float)(self.comp_fonctionpub_max) * self.get_taux_comparaison()
300
301 def get_comp_locale_max_euros(self):
302 return (float)(self.comp_locale_max) * self.get_taux_comparaison()
303
304 def get_comp_ong_max_euros(self):
305 return (float)(self.comp_ong_max) * self.get_taux_comparaison()
306
307 def get_comp_autre_max_euros(self):
308 return (float)(self.comp_autre_max) * self.get_taux_comparaison()
309
310
311 def __unicode__(self):
312 """
313 Cette fonction est consommatrice SQL car elle cherche les dossiers qui ont été liés à celui-ci.
314 """
315 complement_nom_poste = self.get_complement_nom()
316 if complement_nom_poste is None:
317 complement_nom_poste = ""
318 employe = self.get_employe()
319 if employe is None:
320 employe = "VACANT"
321 data = (
322 self.implantation,
323 self.type_poste.nom,
324 self.nom,
325 self.id,
326 complement_nom_poste,
327 employe,
328 )
329 return u'%s - %s (%s) [dae-%s %s %s]' % data
330
331
332 # Tester l'enregistrement car les models.py sont importés au complet
333 if not reversion.is_registered(Poste):
334 reversion.register(Poste)
335
336
337 POSTE_FINANCEMENT_CHOICES = (
338 ('A', 'A - Frais de personnel'),
339 ('B', 'B - Projet(s)-Titre(s)'),
340 ('C', 'C - Autre')
341 )
342
343
344 class PosteFinancement(models.Model):
345 poste = models.ForeignKey('Poste', related_name='financements')
346 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
347 pourcentage = models.DecimalField(max_digits=12, decimal_places=2,
348 help_text="ex.: 33.33 % (décimale avec point)")
349 commentaire = models.TextField(
350 help_text="Spécifiez la source de financement.")
351
352 class Meta:
353 ordering = ['type']
354
355 def __unicode__(self):
356 return u"%s %s %s" % (self.get_type_display(), self.pourcentage, self.commentaire)
357
358 GENRE_CHOICES = (
359 ('m', 'Homme'),
360 ('f', 'Femme'),
361 )
362
363
364 class Employe(models.Model):
365
366 # Modèle existant
367 id_rh = models.ForeignKey(rh.Employe, null=True, related_name='+',
368 verbose_name='Employé')
369 nom = models.CharField(max_length=255)
370 prenom = models.CharField(max_length=255, verbose_name='Prénom')
371 genre = models.CharField(max_length=1, choices=GENRE_CHOICES,
372 null=True, blank=True)
373
374 def __unicode__(self):
375 return u'%s %s' % (self.prenom, self.nom)
376
377
378 COMPTE_COMPTA_CHOICES = (
379 ('coda', 'CODA'),
380 ('scs', 'SCS'),
381 ('aucun', 'Aucun'),
382 )
383
384 class DossierPiece(models.Model):
385 dossier = models.ForeignKey("Dossier")
386 nom = models.CharField(verbose_name="Nom", max_length=255)
387 fichier = models.FileField(verbose_name="Fichier", upload_to=dossier_piece_dispatch, storage=storage_prive)
388
389 class Dossier(DossierWorkflow, models.Model):
390
391 # Modèle existant
392 employe = models.ForeignKey('Employe', related_name='+', editable=False)
393 poste = models.ForeignKey('Poste', related_name='+', editable=False)
394 statut = models.ForeignKey(rh.Statut, related_name='+')
395 organisme_bstg = models.ForeignKey(rh.OrganismeBstg,
396 null=True, blank=True,
397 verbose_name="Organisme",
398 help_text="Si détaché (DET) ou mis à disposition (MAD), \
399 préciser l'organisme.",
400 related_name='+')
401 organisme_bstg_autre = models.CharField(max_length=255,
402 verbose_name="Autre organisme",
403 help_text="indiquer l'organisme ici s'il n'est pas dans la liste",
404 null=True,
405 blank=True,)
406
407 # Données antérieures de l'employé
408 statut_anterieur = models.ForeignKey(
409 rh.Statut, related_name='+', null=True, blank=True,
410 verbose_name='Statut antérieur')
411 classement_anterieur = models.ForeignKey(
412 rh.Classement, related_name='+', null=True, blank=True,
413 verbose_name='Classement précédent')
414 salaire_anterieur = models.DecimalField(
415 max_digits=12, decimal_places=2, null=True, default=None,
416 blank=True, verbose_name='Salaire précédent')
417
418 # Données du titulaire précédent
419 employe_anterieur = models.ForeignKey(
420 rh.Employe, related_name='+', null=True, blank=True,
421 verbose_name='Employé précédent')
422 statut_titulaire_anterieur = models.ForeignKey(
423 rh.Statut, related_name='+', null=True, blank=True,
424 verbose_name='Statut titulaire précédent')
425 classement_titulaire_anterieur = models.ForeignKey(
426 rh.Classement, related_name='+', null=True, blank=True,
427 verbose_name='Classement titulaire précédent')
428 salaire_titulaire_anterieur = models.DecimalField(
429 max_digits=12, decimal_places=2, default=None, null=True,
430 blank=True, verbose_name='Salaire titulaire précédent')
431
432 # Recrutement
433 remplacement = models.BooleanField()
434 statut_residence = models.CharField(max_length=10, default='local',
435 verbose_name="Statut",
436 choices=STATUT_RESIDENCE_CHOICES)
437
438 # Rémunération
439 classement = models.ForeignKey(rh.Classement, related_name='+',
440 verbose_name='Classement proposé')
441 salaire = models.DecimalField(max_digits=12, decimal_places=2,
442 verbose_name='Salaire de base',
443 null=True, default=None)
444 devise = models.ForeignKey(rh.Devise, default=5, related_name='+')
445 regime_travail = models.DecimalField(max_digits=12, decimal_places=2,
446 verbose_name="Régime de travail",
447 help_text="% du temps complet")
448 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
449 decimal_places=2, verbose_name="Nb. heures par semaine")
450
451 # Contrat
452 type_contrat = models.ForeignKey(rh.TypeContrat, related_name='+')
453 contrat_date_debut = models.DateField(help_text="format: aaaa-mm-jj")
454 contrat_date_fin = models.DateField(null=True, blank=True,
455 help_text="format: aaaa-mm-jj")
456
457 # Comptes
458 compte_compta = models.CharField(max_length=10, default='aucun',
459 verbose_name=u'Compte comptabilité',
460 choices=COMPTE_COMPTA_CHOICES)
461 compte_courriel = models.BooleanField()
462
463 # Méta
464 date_creation = models.DateTimeField(auto_now_add=True)
465
466 def __unicode__(self):
467 return u'%s - %s' % (self.poste.nom, self.employe)
468
469 def get_salaire_euros(self):
470 try:
471 tx = rh.TauxChange.objects.filter(implantation=self.poste.implantation, devise=self.devise)[0].taux
472 except:
473 tx = 1
474 return (float)(tx) * (float)(self.salaire)
475
476 def get_couts_auf(self):
477 """
478 On retire les MAD BSTG
479 """
480 return [r for r in self.remuneration_set.all() if r.type_id not in (2, )]
481
482 def get_total_couts_auf(self):
483 total = 0.0
484 for r in self.get_couts_auf():
485 total += r.montant_euro()
486 return total
487
488 def get_aides_auf(self):
489 """
490 On récupère les MAD BSTG
491 """
492 return [r for r in self.remuneration_set.all() if r.type_id in (2, )]
493
494 def get_total_aides_auf(self):
495 total = 0.0
496 for r in self.get_aides_auf():
497 total += r.montant_euro()
498 return total
499
500 def get_total_remun(self):
501 return self.get_total_couts_auf() + self.get_total_aides_auf()
502
503 # Tester l'enregistrement car les models.py sont importés au complet
504 if not reversion.is_registered(Dossier):
505 reversion.register(Dossier)
506
507 class Remuneration(models.Model):
508 # Identification
509 dossier = models.ForeignKey('Dossier', db_column='dossier')
510 type = models.ForeignKey(rh.TypeRemuneration, db_column='type',
511 related_name='+')
512 # TODO: what's that?
513 # type_revalorisation = models.ForeignKey('TypeRevalorisation',
514 # db_column='type_revalorisation')
515 montant = models.DecimalField(max_digits=12, decimal_places=2,
516 null=True) # Annuel
517 devise = models.ForeignKey(rh.Devise, to_field='code',
518 db_column='devise', related_name='+')
519 precision = models.CharField(max_length=255, null=True, blank=True)
520 # date_effective = models.DateField(null=True, blank=True)
521 # pourcentage = models.IntegerField(null=True, blank=True)
522
523 # Méta
524 date_creation = models.DateField(auto_now_add=True)
525 user_creation = models.IntegerField(null=True, blank=True)
526 # desactivation = models.BooleanField(default=False, blank=True)
527 # date_desactivation = models.DateField(null=True, blank=True)
528 # user_desactivation = models.IntegerField(null=True, blank=True)
529 # annulation = models.BooleanField(default=False, blank=True)
530 # date_annulation = models.DateField(null=True, blank=True)
531 # user_annulation = models.IntegerField(null=True, blank=True)
532
533 def montant_mois(self):
534 return round(self.montant / 12, 2)
535
536 def taux_devise(self):
537 return self.devise.tauxchange_set.order_by('-annee').all()[0].taux
538
539 def montant_euro(self):
540 return round(float(self.montant) * float(self.taux_devise()), 2)
541
542 def montant_euro_mois(self):
543 return round(self.montant_euro() / 12, 2)
544
545
546 TYPE_JUSTIFICATIONS = (
547 ('N', 'Nouvel employé'),
548 ('R', 'Renouvellement employé'),
549 )
550
551 class JustificationQuestion(models.Model):
552 question = models.CharField(max_length=255)
553 type = models.CharField(max_length=255, choices=TYPE_JUSTIFICATIONS)
554
555 def __unicode__(self,):
556 return self.question
557
558 class JustificationNouvelEmploye(models.Model):
559 dossier = models.ForeignKey("Dossier")
560 question = models.ForeignKey("JustificationQuestion")
561 reponse = models.TextField()
562
563 class JustificationAutreEmploye(models.Model):
564 dossier = models.ForeignKey("Dossier")
565 question = models.ForeignKey("JustificationQuestion")
566 reponse = models.TextField()