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