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