eba318de00478b0d9ce8ad9231dee7616017b6ff
[auf_rh_dae.git] / project / dae / models.py
1 # -=- encoding: utf-8 -=-
2
3 from django.conf import settings
4 from django.core.files.storage import FileSystemStorage
5 from django.db import models
6 import reversion
7 from rh import models as rh
8 from workflow import PosteWorkflow, DossierWorkflow
9 from workflow import DOSSIER_ETAT_DRH_FINALISATION, DOSSIER_ETAT_REGION_FINALISATION, \
10 DOSSIER_ETAT_FINALISE
11 from auf.django.metadata.models import AUFMetadata
12 from managers import *
13 from rh.models import HELP_TEXT_DATE
14
15
16 # Upload de fichiers
17 UPLOAD_STORAGE = FileSystemStorage(settings.PRIVE_MEDIA_ROOT)
18
19
20 ### POSTE
21
22 POSTE_APPEL_CHOICES = (
23 ('interne', 'Interne'),
24 ('externe', 'Externe'),
25 )
26 POSTE_ACTION = (
27 ('N', u"Nouveau poste"),
28 ('M', u"Poste existant"),
29 ('E', u"Évolution de poste"),
30 )
31
32
33 class DeviseException(Exception):
34 silent_variable_failure = True
35
36
37 class Poste(PosteWorkflow, rh.Poste_):
38
39 type_intervention = models.CharField(max_length=1, choices=POSTE_ACTION, default='N')
40
41 # Modèle existant
42 id_rh = models.ForeignKey(rh.Poste, null=True, related_name='+',
43 editable=False,
44 verbose_name=u"Mise à jour du poste")
45
46 # Rémunération
47 indemn_expat_min = models.DecimalField(max_digits=13, decimal_places=2, default=0)
48 indemn_expat_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
49 indemn_fct_min = models.DecimalField(max_digits=12, decimal_places=2, default=0)
50 indemn_fct_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
51 charges_patronales_min = models.DecimalField(max_digits=12, decimal_places=2, default=0)
52 charges_patronales_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
53
54 # Managers
55 objects = PosteManager()
56
57
58 def est_importe(self):
59 """Test si le poste a déjà été importé"""
60 return ImportPoste.objects.filter(dae=self).exists()
61
62 def importer(self):
63 if self.est_importe():
64 return ImportPoste.objects.get(dae=self)
65 rh_poste = rh.Poste()
66 # Faire une copie profonde de l'objet.
67 # PosteFinancement, PosteComparaison, Remun modele a ajuster...
68
69 def copy_model(src, dst, exclude=[]):
70 keys = [f.name for f in src._meta.fields if f.name not in ['id', ] + exclude]
71 for k in keys:
72 setattr(dst, k, getattr(src, k))
73 return dst
74
75 rh_poste = copy_model(self, rh_poste)
76 rh_poste.save()
77 print rh_poste.id
78
79 for o in self.dae_financements.all():
80 rh_financement = rh.PosteFinancement()
81 rh_financement = copy_model(o, rh_financement, exclude=['poste',])
82 rh_financement.poste = rh_poste
83 rh_financement.save()
84
85 for o in self.dae_pieces.all():
86 rh_piece = rh.PostePiece()
87 rh_piece = copy_model(o, rh_piece, exclude=['poste',])
88 rh_piece.poste = rh_poste
89 rh_piece.save()
90
91 for o in self.dae_comparaisons_internes.all():
92 rh_comp = rh.PosteComparaison()
93 rh_comp = copy_model(o, rh_financement, exclude=['poste',])
94 rh_comp.poste = rh_poste
95 rh_comp.save()
96
97 return rh_poste
98
99 def _get_key(self):
100 """
101 Les vues sont montées selon une clef spéciale
102 pour identifier la provenance du poste.
103 Cette méthode fournit un moyen de reconstruire cette clef
104 afin de générer les URLs.
105 """
106 return "dae-%s" % self.id
107 key = property(_get_key)
108
109 def get_dossiers(self):
110 """
111 Liste tous les anciens dossiers liés à ce poste.
112 (Le nom de la relation sur le rh.Poste est mal choisi
113 poste1 au lieu de dossier1)
114 Note1 : seulement le dosssier principal fait l'objet de la recherche.
115 Note2 : les dossiers sont retournés du plus récent au plus vieux.
116 (Ce test est fait en fonction du id,
117 car les dates de création sont absentes de rh v1).
118 """
119 if self.id_rh is None:
120 return []
121 return self.id_rh.rh_dossiers.all()
122
123
124 def get_employe(self):
125 """
126 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
127 """
128 dossiers = self.get_dossiers()
129 if len(dossiers) > 0:
130 return dossiers[0].employe
131 else:
132 return None
133
134 def get_default_devise(self):
135 """Récupère la devise par défaut en fonction de l'implantation
136 (EUR autrement)
137 """
138 try:
139 implantation_devise = rh.TauxChange.objects \
140 .filter(implantation=self.implantation)[0].devise
141 except:
142 implantation_devise = 5 # EUR
143 return implantation_devise
144
145 #####################
146 # Classement de poste
147 #####################
148
149 def get_couts_minimum(self):
150 return self.salaire_min + self.indemn_expat_min + self.indemn_fct_min + self.charges_patronales_min + self.autre_min
151
152 def get_salaire_minimum(self):
153 return self.get_couts_minimum() - self.charges_patronales_min
154
155 def get_taux_minimum(self):
156 if self.devise_min.code == 'EUR':
157 return 1
158 liste_taux = self.devise_min.tauxchange_set.order_by('-annee')
159 if len(liste_taux) == 0:
160 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_min, self.implantation))
161 else:
162 return liste_taux[0].taux
163
164 def get_couts_minimum_euros(self):
165 return float(self.get_couts_minimum()) * self.get_taux_minimum()
166
167 def get_salaire_minimum_euros(self):
168 return float(self.get_salaire_minimum()) * self.get_taux_minimum()
169
170 def get_couts_maximum(self):
171 return self.salaire_max + self.indemn_expat_max + self.indemn_fct_max + self.charges_patronales_max + self.autre_max
172
173 def get_salaire_maximum(self):
174 return self.get_couts_maximum() - self.charges_patronales_max
175
176 def get_taux_maximum(self):
177 if self.devise_max.code == 'EUR':
178 return 1
179 liste_taux = self.devise_max.tauxchange_set.order_by('-annee')
180 if len(liste_taux) == 0:
181 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_max, self.implantation))
182 else:
183 return liste_taux[0].taux
184
185 def get_couts_maximum_euros(self):
186 return float(self.get_couts_maximum()) * self.get_taux_maximum()
187
188 def get_salaire_maximum_euros(self):
189 return float(self.get_salaire_maximum()) * self.get_taux_maximum()
190
191 def show_taux_minimum(self):
192 try:
193 return self.get_taux_minimum()
194 except DeviseException, e:
195 return e
196
197 def show_couts_minimum_euros(self):
198 try:
199 return self.get_couts_minimum_euros()
200 except DeviseException, e:
201 return e
202
203 def show_salaire_minimum_euros(self):
204 try:
205 return self.get_salaire_minimum_euros()
206 except DeviseException, e:
207 return e
208
209 def show_taux_maximum(self):
210 try:
211 return self.get_taux_maximum()
212 except DeviseException, e:
213 return e
214
215 def show_couts_maximum_euros(self):
216 try:
217 return self.get_couts_maximum_euros()
218 except DeviseException, e:
219 return e
220
221 def show_salaire_maximum_euros(self):
222 try:
223 return self.get_salaire_maximum_euros()
224 except DeviseException, e:
225 return e
226
227
228 ######################
229 # Comparaison de poste
230 ######################
231
232 def est_comparable(self):
233 """
234 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
235 est comparable.
236 """
237 if self.comp_universite_min is None and \
238 self.comp_fonctionpub_min is None and \
239 self.comp_locale_min is None and \
240 self.comp_ong_min is None and \
241 self.comp_autre_min is None and \
242 self.comp_universite_max is None and \
243 self.comp_fonctionpub_max is None and \
244 self.comp_locale_max is None and \
245 self.comp_ong_max is None and \
246 self.comp_autre_max is None:
247 return False
248 else:
249 return True
250
251
252 def get_taux_comparaison(self):
253 try:
254 return rh.TauxChange.objects.filter(devise=self.devise_comparaison)[0].taux
255 except:
256 return 1
257
258 def get_comp_universite_min_euros(self):
259 return (float)(self.comp_universite_min) * self.get_taux_comparaison()
260
261 def get_comp_fonctionpub_min_euros(self):
262 return (float)(self.comp_fonctionpub_min) * self.get_taux_comparaison()
263
264 def get_comp_locale_min_euros(self):
265 return (float)(self.comp_locale_min) * self.get_taux_comparaison()
266
267 def get_comp_ong_min_euros(self):
268 return (float)(self.comp_ong_min) * self.get_taux_comparaison()
269
270 def get_comp_autre_min_euros(self):
271 return (float)(self.comp_autre_min) * self.get_taux_comparaison()
272
273 def get_comp_universite_max_euros(self):
274 return (float)(self.comp_universite_max) * self.get_taux_comparaison()
275
276 def get_comp_fonctionpub_max_euros(self):
277 return (float)(self.comp_fonctionpub_max) * self.get_taux_comparaison()
278
279 def get_comp_locale_max_euros(self):
280 return (float)(self.comp_locale_max) * self.get_taux_comparaison()
281
282 def get_comp_ong_max_euros(self):
283 return (float)(self.comp_ong_max) * self.get_taux_comparaison()
284
285 def get_comp_autre_max_euros(self):
286 return (float)(self.comp_autre_max) * self.get_taux_comparaison()
287
288
289 def __unicode__(self):
290 """
291 Cette fonction est consommatrice SQL car elle cherche les dossiers
292 qui ont été liés à celui-ci.
293 """
294 data = (
295 self.implantation,
296 self.type_poste.nom,
297 self.nom,
298 )
299 return u'%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 class PosteFinancement(rh.PosteFinancement_):
314 pass
315
316 class PostePiece(rh.PostePiece_):
317 pass
318
319 class PosteComparaison(rh.PosteComparaison_):
320 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name=u'Statut', null=True, blank=True, )
321 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name=u'Classement', null=True, blank=True, )
322
323 ### EMPLOYÉ/PERSONNE
324
325 # TODO : migration pour m -> M, f -> F
326
327 GENRE_CHOICES = (
328 ('m', 'Homme'),
329 ('f', 'Femme'),
330 )
331
332 class Employe(AUFMetadata):
333
334 # Modèle existant
335 id_rh = models.ForeignKey(rh.Employe, null=True, related_name='+',
336 verbose_name=u'Employé')
337 nom = models.CharField(max_length=255)
338 prenom = models.CharField(max_length=255, verbose_name=u'Prénom')
339 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
340
341 def __unicode__(self):
342 return u'%s %s' % (self.prenom, self.nom.upper())
343
344
345 ### DOSSIER
346
347 STATUT_RESIDENCE_CHOICES = (
348 ('local', 'Local'),
349 ('expat', 'Expatrié'),
350 )
351
352 COMPTE_COMPTA_CHOICES = (
353 ('coda', 'CODA'),
354 ('scs', 'SCS'),
355 ('aucun', 'Aucun'),
356 )
357
358 class Dossier(DossierWorkflow, rh.Dossier_):
359 poste = models.ForeignKey('Poste', db_column='poste', related_name='%(app_label)s_dossiers')
360 employe = models.ForeignKey('Employe', db_column='employe',
361 related_name='%(app_label)s_dossiers',
362 verbose_name=u"Employé")
363 organisme_bstg_autre = models.CharField(max_length=255,
364 verbose_name=u"Autre organisme",
365 help_text="indiquer l'organisme ici s'il n'est pas dans la liste",
366 null=True,
367 blank=True,)
368
369 # Données antérieures de l'employé
370 statut_anterieur = models.ForeignKey(
371 rh.Statut, related_name='+', null=True, blank=True,
372 verbose_name=u'Statut antérieur')
373 classement_anterieur = models.ForeignKey(
374 rh.Classement, related_name='+', null=True, blank=True,
375 verbose_name=u'Classement précédent')
376 salaire_anterieur = models.DecimalField(
377 max_digits=12, decimal_places=2, null=True, default=None,
378 blank=True, verbose_name=u'Salaire précédent')
379 devise_anterieur = models.ForeignKey(rh.Devise, related_name='+',
380 null=True, blank=True)
381 type_contrat_anterieur = models.ForeignKey(rh.TypeContrat,
382 related_name='+', null=True, blank=True,
383 verbose_name=u'Type contrat antérieur', )
384
385 # Données du titulaire précédent
386 employe_anterieur = models.ForeignKey(
387 rh.Employe, related_name='+', null=True, blank=True,
388 verbose_name=u'Employé précédent')
389 statut_titulaire_anterieur = models.ForeignKey(
390 rh.Statut, related_name='+', null=True, blank=True,
391 verbose_name=u'Statut titulaire précédent')
392 classement_titulaire_anterieur = models.ForeignKey(
393 rh.Classement, related_name='+', null=True, blank=True,
394 verbose_name=u'Classement titulaire précédent')
395 salaire_titulaire_anterieur = models.DecimalField(
396 max_digits=12, decimal_places=2, default=None, null=True,
397 blank=True, verbose_name=u'Salaire titulaire précédent')
398 devise_titulaire_anterieur = models.ForeignKey(rh.Devise, related_name='+', null=True, blank=True)
399
400 # Rémunération
401 salaire = models.DecimalField(max_digits=13, decimal_places=2,
402 verbose_name=u'Salaire de base',
403 null=True, default=None)
404 devise = models.ForeignKey(rh.Devise, default=5, related_name='+')
405
406 # Contrat
407 type_contrat = models.ForeignKey(rh.TypeContrat, related_name='+')
408 contrat_date_debut = models.DateField(help_text=HELP_TEXT_DATE)
409 contrat_date_fin = models.DateField(null=True, blank=True,
410 help_text=HELP_TEXT_DATE)
411
412 # Justifications
413 justif_nouveau_statut_label = u'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
414 justif_nouveau_statut = models.TextField(verbose_name=justif_nouveau_statut_label, null=True, blank=True)
415 justif_nouveau_tmp_remplacement_label = u"Si l'employé effectue un remplacement temporaire, préciser"
416 justif_nouveau_tmp_remplacement = models.TextField(verbose_name=justif_nouveau_tmp_remplacement_label, null=True, blank=True)
417 justif_nouveau_salaire_label = u"Si le salaire de l'employé ne correspond pas au classement du poste ou est différent du salaire antérieur, justifier "
418 justif_nouveau_salaire = models.TextField(verbose_name=justif_nouveau_salaire_label, null=True, blank=True)
419 justif_nouveau_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
420 justif_nouveau_commentaire = models.TextField(verbose_name=justif_nouveau_commentaire_label, null=True, blank=True)
421 justif_rempl_type_contrat_label = u"Changement de type de contrat, ex : d'un CDD en CDI"
422 justif_rempl_type_contrat = models.TextField(verbose_name=justif_rempl_type_contrat_label, null=True, blank=True)
423 justif_rempl_statut_employe_label = u"Si le statut de l'employé a été modifié pour ce poste ; ex :national, expatrié, màd, détachement ? Si oui, justifier"
424 justif_rempl_statut_employe = models.TextField(verbose_name=justif_rempl_statut_employe_label, null=True, blank=True)
425 justif_rempl_evaluation_label = u"L'évaluation de l'employé est-elle favorable? Préciser"
426 justif_rempl_evaluation = models.TextField(verbose_name=justif_rempl_evaluation_label, null=True, blank=True)
427 justif_rempl_salaire_label = u"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
428 justif_rempl_salaire = models.TextField(verbose_name=justif_rempl_salaire_label, null=True, blank=True)
429 justif_rempl_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
430 justif_rempl_commentaire = models.TextField(verbose_name=justif_rempl_commentaire_label, null=True, blank=True)
431
432 # Comptes
433 compte_compta = models.CharField(max_length=10, default='aucun',
434 verbose_name=u'Compte comptabilité',
435 choices=COMPTE_COMPTA_CHOICES)
436 compte_courriel = models.BooleanField()
437
438 # DAE numérisée
439 dae_numerisee = models.FileField(upload_to='dae/dae_numerisee', storage=UPLOAD_STORAGE,
440 blank=True, null=True, verbose_name="DAE numérisée")
441
442 # Managers
443 objects = DossierManager()
444
445 def __init__(self, *args, **kwargs):
446 # Bouchon pour créer une date fictive necessaire pour valider un dossier
447 # à cause de l'héritage
448 super(rh.Dossier_, self).__init__(*args, **kwargs)
449 super(DossierWorkflow, self).__init__(*args, **kwargs)
450 import datetime
451 self.date_debut = datetime.datetime.today()
452
453 def __unicode__(self):
454 return u'[%s] %s - %s' % (self.poste.implantation, self.poste.nom, self.employe)
455
456 def est_importe(self):
457 """Test si le dossier a déjà été importé"""
458 return dae.ImportDossier.objects.filter(dae=self).exists()
459
460 def importer(self):
461 if not self.poste.est_importe():
462 raise Exception('Le poste de cette DAE doît être importé')
463 return True
464
465 def get_salaire_anterieur_euros(self):
466 if self.devise_anterieur is None:
467 return None
468 try:
469 taux = self.taux_devise(self.devise_anterieur)
470 except Exception, e:
471 return e
472 if not taux:
473 return None
474 return int(round(float(self.salaire_anterieur) * float(taux), 2))
475
476
477 def get_salaire_titulaire_anterieur_euros(self):
478 if self.devise_titulaire_anterieur is None:
479 return None
480 try:
481 taux = self.taux_devise(self.devise_titulaire_anterieur)
482 except Exception, e:
483 return e
484 if not taux:
485 return None
486 return int(round(float(self.salaire_titulaire_anterieur) * float(taux), 2))
487
488 def get_salaire_euros(self):
489 tx = self.taux_devise()
490 return (float)(tx) * (float)(self.salaire)
491
492 def get_remunerations_brutes(self):
493 """
494 1 Salaire de base
495 3 Indemnité de base
496 4 Indemnité d'expatriation
497 5 Indemnité pour frais
498 6 Indemnité de logement
499 7 Indemnité de fonction
500 8 Indemnité de responsabilité
501 9 Indemnité de transport
502 10 Indemnité compensatrice
503 11 Indemnité de subsistance
504 12 Indemnité différentielle
505 13 Prime d'installation
506 14 Billet d'avion
507 15 Déménagement
508 16 Indemnité de départ
509 18 Prime de 13ième mois
510 19 Prime d'intérim
511 """
512 ids = [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
513 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
514
515 def get_charges_salariales(self):
516 """
517 20 Charges salariales ?
518 """
519 ids = [20, ]
520 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
521
522 def get_total_charges_salariales(self):
523 total = 0.0
524 for r in self.get_charges_salariales():
525 total += r.montant_euros()
526 return total
527
528 def get_charges_patronales(self):
529 """
530 17 Charges patronales
531 """
532 ids = [17, ]
533 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
534
535 def get_total_charges_patronales(self):
536 total = 0.0
537 for r in self.get_charges_patronales():
538 total += r.montant_euros()
539 return total
540
541 def get_salaire_brut(self):
542 """
543 somme des rémuérations brutes
544 """
545 total = 0.0
546 for r in self.get_remunerations_brutes():
547 total += r.montant_euros()
548 return total
549
550 def get_salaire_net(self):
551 """
552 salaire brut - charges salariales
553 """
554 total_charges = 0.0
555 for r in self.get_charges_salariales():
556 total_charges += r.montant_euros()
557 return self.get_salaire_brut() - total_charges
558
559 def get_couts_auf(self):
560 """
561 salaire net + charges patronales
562 """
563 total_charges = 0.0
564 for r in self.get_charges_patronales():
565 total_charges += r.montant_euros()
566 return self.get_salaire_net() + total_charges
567
568 def get_remunerations_tierces(self):
569 """
570 2 Salaire MAD
571 """
572 return [r for r in self.dae_remunerations.all() if r.type_id in (2, )]
573
574 def get_total_remunerations_tierces(self):
575 total = 0.0
576 for r in self.get_remunerations_tierces():
577 total += r.montant_euros()
578 return total
579
580 def valide(self):
581 return self.etat in (DOSSIER_ETAT_REGION_FINALISATION,
582 DOSSIER_ETAT_DRH_FINALISATION,
583 DOSSIER_ETAT_FINALISE)
584
585
586 # Tester l'enregistrement car les models.py sont importés au complet
587 if not reversion.is_registered(Dossier):
588 reversion.register(Dossier)
589
590 class DossierPiece(rh.DossierPiece_):
591 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
592 Ex.: Lettre de motivation.
593 """
594 pass
595
596 class DossierComparaison(rh.DossierComparaison_):
597 """
598 Photo d'une comparaison salariale au moment de l'embauche.
599 """
600 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name='Statut', null=True, blank=True, )
601 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name='Classement', null=True, blank=True, )
602
603
604 ### RÉMUNÉRATION
605
606 class Remuneration(rh.Remuneration_):
607 pass
608
609 ### CONTRATS
610
611 class Contrat(rh.Contrat_):
612 pass
613
614 # modèle de liaison entre les systèmes
615
616 class ImportDossier(models.Model):
617 dae = models.ForeignKey('dae.Dossier', related_name='+')
618 rh = models.ForeignKey('rh.Dossier', related_name='+')
619
620 class ImportPoste(models.Model):
621 dae = models.ForeignKey('dae.Poste', related_name='+')
622 rh = models.ForeignKey('rh.Poste', related_name='+')