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