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