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