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