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