devise on current year
[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 from django.db.models import AutoField
64 if self.est_importe():
65 return ImportPoste.objects.get(dae=self)
66 rh_poste = rh.Poste()
67 # Faire une copie profonde de l'objet.
68 # PosteFinancement, PosteComparaison, Remun modele a ajuster...
69
70 def copy_model(src, dst, exclude=[]):
71 keys = [f.name for f in src._meta.fields if f.name not in ['id', ] + exclude]
72 for k in keys:
73 setattr(dst, k, getattr(src, k))
74 return dst
75
76 rh_poste = copy_model(self, rh_poste)
77 rh_poste.save()
78 print rh_poste.id
79
80 for o in self.dae_financements.all():
81 rh_financement = rh.PosteFinancement()
82 rh_financement = copy_model(o, rh_financement, exclude=['poste',])
83 rh_financement.poste = rh_poste
84 rh_financement.save()
85
86 for o in self.dae_pieces.all():
87 rh_piece = rh.PostePiece()
88 rh_piece = copy_model(o, rh_piece, exclude=['poste',])
89 rh_piece.poste = rh_poste
90 rh_piece.save()
91
92 for o in self.dae_comparaisons_internes.all():
93 rh_comp = rh.PosteComparaison()
94 rh_comp = copy_model(o, rh_financement, exclude=['poste',])
95 rh_comp.poste = rh_poste
96 rh_comp.save()
97
98 return rh_poste
99
100 def _get_key(self):
101 """
102 Les vues sont montées selon une clef spéciale
103 pour identifier la provenance du poste.
104 Cette méthode fournit un moyen de reconstruire cette clef
105 afin de générer les URLs.
106 """
107 return "dae-%s" % self.id
108 key = property(_get_key)
109
110 def get_dossiers(self):
111 """
112 Liste tous les anciens dossiers liés à ce poste.
113 (Le nom de la relation sur le rh.Poste est mal choisi
114 poste1 au lieu de dossier1)
115 Note1 : seulement le dosssier principal fait l'objet de la recherche.
116 Note2 : les dossiers sont retournés du plus récent au plus vieux.
117 (Ce test est fait en fonction du id,
118 car les dates de création sont absentes de rh v1).
119 """
120 if self.id_rh is None:
121 return []
122 return self.id_rh.rh_dossiers.all()
123
124
125 def get_employe(self):
126 """
127 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
128 """
129 dossiers = self.get_dossiers()
130 if len(dossiers) > 0:
131 return dossiers[0].employe
132 else:
133 return None
134
135 def get_default_devise(self):
136 """Récupère la devise par défaut en fonction de l'implantation
137 (EUR autrement)
138 """
139 try:
140 implantation_devise = rh.TauxChange.objects \
141 .filter(implantation=self.implantation)[0].devise
142 except:
143 implantation_devise = 5 # EUR
144 return implantation_devise
145
146 #####################
147 # Classement de poste
148 #####################
149
150 def get_couts_minimum(self):
151 return self.salaire_min + self.indemn_expat_min + self.indemn_fct_min + self.charges_patronales_min + self.autre_min
152
153 def get_salaire_minimum(self):
154 return self.get_couts_minimum() - self.charges_patronales_min
155
156 def get_taux_minimum(self):
157 if self.devise_min.code == 'EUR':
158 return 1
159 liste_taux = self.devise_min.tauxchange_set.order_by('-annee')
160 if len(liste_taux) == 0:
161 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_min, self.implantation))
162 else:
163 return liste_taux[0].taux
164
165 def get_couts_minimum_euros(self):
166 return float(self.get_couts_minimum()) * self.get_taux_minimum()
167
168 def get_salaire_minimum_euros(self):
169 return float(self.get_salaire_minimum()) * self.get_taux_minimum()
170
171 def get_couts_maximum(self):
172 return self.salaire_max + self.indemn_expat_max + self.indemn_fct_max + self.charges_patronales_max + self.autre_max
173
174 def get_salaire_maximum(self):
175 return self.get_couts_maximum() - self.charges_patronales_max
176
177 def get_taux_maximum(self):
178 if self.devise_max.code == 'EUR':
179 return 1
180 liste_taux = self.devise_max.tauxchange_set.order_by('-annee')
181 if len(liste_taux) == 0:
182 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_max, self.implantation))
183 else:
184 return liste_taux[0].taux
185
186 def get_couts_maximum_euros(self):
187 return float(self.get_couts_maximum()) * self.get_taux_maximum()
188
189 def get_salaire_maximum_euros(self):
190 return float(self.get_salaire_maximum()) * self.get_taux_maximum()
191
192 def show_taux_minimum(self):
193 try:
194 return self.get_taux_minimum()
195 except DeviseException, e:
196 return e
197
198 def show_couts_minimum_euros(self):
199 try:
200 return self.get_couts_minimum_euros()
201 except DeviseException, e:
202 return e
203
204 def show_salaire_minimum_euros(self):
205 try:
206 return self.get_salaire_minimum_euros()
207 except DeviseException, e:
208 return e
209
210 def show_taux_maximum(self):
211 try:
212 return self.get_taux_maximum()
213 except DeviseException, e:
214 return e
215
216 def show_couts_maximum_euros(self):
217 try:
218 return self.get_couts_maximum_euros()
219 except DeviseException, e:
220 return e
221
222 def show_salaire_maximum_euros(self):
223 try:
224 return self.get_salaire_maximum_euros()
225 except DeviseException, e:
226 return e
227
228
229 ######################
230 # Comparaison de poste
231 ######################
232
233 def est_comparable(self):
234 """
235 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
236 est comparable.
237 """
238 if self.comp_universite_min is None and \
239 self.comp_fonctionpub_min is None and \
240 self.comp_locale_min is None and \
241 self.comp_ong_min is None and \
242 self.comp_autre_min is None and \
243 self.comp_universite_max is None and \
244 self.comp_fonctionpub_max is None and \
245 self.comp_locale_max is None and \
246 self.comp_ong_max is None and \
247 self.comp_autre_max is None:
248 return False
249 else:
250 return True
251
252
253 def get_taux_comparaison(self):
254 try:
255 return rh.TauxChange.objects.filter(devise=self.devise_comparaison)[0].taux
256 except:
257 return 1
258
259 def get_comp_universite_min_euros(self):
260 return (float)(self.comp_universite_min) * self.get_taux_comparaison()
261
262 def get_comp_fonctionpub_min_euros(self):
263 return (float)(self.comp_fonctionpub_min) * self.get_taux_comparaison()
264
265 def get_comp_locale_min_euros(self):
266 return (float)(self.comp_locale_min) * self.get_taux_comparaison()
267
268 def get_comp_ong_min_euros(self):
269 return (float)(self.comp_ong_min) * self.get_taux_comparaison()
270
271 def get_comp_autre_min_euros(self):
272 return (float)(self.comp_autre_min) * self.get_taux_comparaison()
273
274 def get_comp_universite_max_euros(self):
275 return (float)(self.comp_universite_max) * self.get_taux_comparaison()
276
277 def get_comp_fonctionpub_max_euros(self):
278 return (float)(self.comp_fonctionpub_max) * self.get_taux_comparaison()
279
280 def get_comp_locale_max_euros(self):
281 return (float)(self.comp_locale_max) * self.get_taux_comparaison()
282
283 def get_comp_ong_max_euros(self):
284 return (float)(self.comp_ong_max) * self.get_taux_comparaison()
285
286 def get_comp_autre_max_euros(self):
287 return (float)(self.comp_autre_max) * self.get_taux_comparaison()
288
289
290 def __unicode__(self):
291 """
292 Cette fonction est consommatrice SQL car elle cherche les dossiers
293 qui ont été liés à celui-ci.
294 """
295 data = (
296 self.implantation,
297 self.type_poste.nom,
298 self.nom,
299 )
300 return u'%s - %s (%s)' % data
301
302
303 # Tester l'enregistrement car les models.py sont importés au complet
304 if not reversion.is_registered(Poste):
305 reversion.register(Poste)
306
307
308 POSTE_FINANCEMENT_CHOICES = (
309 ('A', 'A - Frais de personnel'),
310 ('B', 'B - Projet(s)-Titre(s)'),
311 ('C', 'C - Autre')
312 )
313
314 class PosteFinancement(rh.PosteFinancement_):
315 pass
316
317 class PostePiece(rh.PostePiece_):
318 pass
319
320 class PosteComparaison(rh.PosteComparaison_):
321 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name=u'Statut', null=True, blank=True, )
322 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name=u'Classement', null=True, blank=True, )
323
324 ### EMPLOYÉ/PERSONNE
325
326 # TODO : migration pour m -> M, f -> F
327
328 GENRE_CHOICES = (
329 ('m', 'Homme'),
330 ('f', 'Femme'),
331 )
332
333 class Employe(AUFMetadata):
334
335 # Modèle existant
336 id_rh = models.ForeignKey(rh.Employe, null=True, related_name='+',
337 verbose_name=u'Employé')
338 nom = models.CharField(max_length=255)
339 prenom = models.CharField(max_length=255, verbose_name=u'Prénom')
340 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
341
342 def __unicode__(self):
343 return u'%s %s' % (self.prenom, self.nom.upper())
344
345
346 ### DOSSIER
347
348 STATUT_RESIDENCE_CHOICES = (
349 ('local', 'Local'),
350 ('expat', 'Expatrié'),
351 )
352
353 COMPTE_COMPTA_CHOICES = (
354 ('coda', 'CODA'),
355 ('scs', 'SCS'),
356 ('aucun', 'Aucun'),
357 )
358
359 class Dossier(DossierWorkflow, rh.Dossier_):
360 poste = models.ForeignKey('Poste', db_column='poste', related_name='%(app_label)s_dossiers')
361 employe = models.ForeignKey('Employe', db_column='employe',
362 related_name='%(app_label)s_dossiers',
363 verbose_name=u"Employé")
364 organisme_bstg_autre = models.CharField(max_length=255,
365 verbose_name=u"Autre organisme",
366 help_text="indiquer l'organisme ici s'il n'est pas dans la liste",
367 null=True,
368 blank=True,)
369
370 # Données antérieures de l'employé
371 statut_anterieur = models.ForeignKey(
372 rh.Statut, related_name='+', null=True, blank=True,
373 verbose_name=u'Statut antérieur')
374 classement_anterieur = models.ForeignKey(
375 rh.Classement, related_name='+', null=True, blank=True,
376 verbose_name=u'Classement précédent')
377 salaire_anterieur = models.DecimalField(
378 max_digits=12, decimal_places=2, null=True, default=None,
379 blank=True, verbose_name=u'Salaire précédent')
380 devise_anterieur = models.ForeignKey(rh.Devise, related_name='+',
381 null=True, blank=True)
382 type_contrat_anterieur = models.ForeignKey(rh.TypeContrat,
383 related_name='+', null=True, blank=True,
384 verbose_name=u'Type contrat antérieur', )
385
386 # Données du titulaire précédent
387 employe_anterieur = models.ForeignKey(
388 rh.Employe, related_name='+', null=True, blank=True,
389 verbose_name=u'Employé précédent')
390 statut_titulaire_anterieur = models.ForeignKey(
391 rh.Statut, related_name='+', null=True, blank=True,
392 verbose_name=u'Statut titulaire précédent')
393 classement_titulaire_anterieur = models.ForeignKey(
394 rh.Classement, related_name='+', null=True, blank=True,
395 verbose_name=u'Classement titulaire précédent')
396 salaire_titulaire_anterieur = models.DecimalField(
397 max_digits=12, decimal_places=2, default=None, null=True,
398 blank=True, verbose_name=u'Salaire titulaire précédent')
399 devise_titulaire_anterieur = models.ForeignKey(rh.Devise, related_name='+', null=True, blank=True)
400
401 # Rémunération
402 salaire = models.DecimalField(max_digits=13, decimal_places=2,
403 verbose_name=u'Salaire de base',
404 null=True, default=None)
405 devise = models.ForeignKey(rh.Devise, default=5, related_name='+')
406
407 # Contrat
408 type_contrat = models.ForeignKey(rh.TypeContrat, related_name='+')
409 contrat_date_debut = models.DateField(help_text=HELP_TEXT_DATE)
410 contrat_date_fin = models.DateField(null=True, blank=True,
411 help_text=HELP_TEXT_DATE)
412
413 # Justifications
414 justif_nouveau_statut_label = u'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
415 justif_nouveau_statut = models.TextField(verbose_name=justif_nouveau_statut_label, null=True, blank=True)
416 justif_nouveau_tmp_remplacement_label = u"Si l'employé effectue un remplacement temporaire, préciser"
417 justif_nouveau_tmp_remplacement = models.TextField(verbose_name=justif_nouveau_tmp_remplacement_label, null=True, blank=True)
418 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 "
419 justif_nouveau_salaire = models.TextField(verbose_name=justif_nouveau_salaire_label, null=True, blank=True)
420 justif_nouveau_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
421 justif_nouveau_commentaire = models.TextField(verbose_name=justif_nouveau_commentaire_label, null=True, blank=True)
422 justif_rempl_type_contrat_label = u"Changement de type de contrat, ex : d'un CDD en CDI"
423 justif_rempl_type_contrat = models.TextField(verbose_name=justif_rempl_type_contrat_label, null=True, blank=True)
424 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"
425 justif_rempl_statut_employe = models.TextField(verbose_name=justif_rempl_statut_employe_label, null=True, blank=True)
426 justif_rempl_evaluation_label = u"L'évaluation de l'employé est-elle favorable? Préciser"
427 justif_rempl_evaluation = models.TextField(verbose_name=justif_rempl_evaluation_label, null=True, blank=True)
428 justif_rempl_salaire_label = u"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
429 justif_rempl_salaire = models.TextField(verbose_name=justif_rempl_salaire_label, null=True, blank=True)
430 justif_rempl_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
431 justif_rempl_commentaire = models.TextField(verbose_name=justif_rempl_commentaire_label, null=True, blank=True)
432
433 # Comptes
434 compte_compta = models.CharField(max_length=10, default='aucun',
435 verbose_name=u'Compte comptabilité',
436 choices=COMPTE_COMPTA_CHOICES)
437 compte_courriel = models.BooleanField()
438
439 # DAE numérisée
440 dae_numerisee = models.FileField(upload_to='dae/dae_numerisee', storage=UPLOAD_STORAGE,
441 blank=True, null=True, verbose_name="DAE numérisée")
442
443 # Managers
444 objects = DossierManager()
445
446 def __init__(self, *args, **kwargs):
447 # Bouchon pour créer une date fictive necessaire pour valider un dossier
448 # à cause de l'héritage
449 super(rh.Dossier_, self).__init__(*args, **kwargs)
450 super(DossierWorkflow, self).__init__(*args, **kwargs)
451 import datetime
452 self.date_debut = datetime.datetime.today()
453
454 def __unicode__(self):
455 return u'[%s] %s - %s' % (self.poste.implantation, self.poste.nom, self.employe)
456
457 def est_importe(self):
458 """Test si le dossier a déjà été importé"""
459 return dae.ImportDossier.objects.filter(dae=self).exists()
460
461 def importer(self):
462 if not self.poste.est_importe():
463 raise Exception('Le poste de cette DAE doît être importé')
464 return True
465
466 def get_salaire_anterieur_euros(self):
467 if self.devise_anterieur.code == 'EUR':
468 tx = 1
469 else:
470 liste_taux = self.devise_anterieur.tauxchange_set.order_by('-annee')
471 if len(liste_taux) == 0:
472 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_anterieur, self.poste.implantation))
473 tx = liste_taux[0].taux
474 return (float)(tx) * (float)(self.salaire_anterieur)
475
476 def get_salaire_titulaire_anterieur_euros(self):
477 if self.devise_titulaire_anterieur is None:
478 return None
479 if self.devise_titulaire_anterieur.code == 'EUR':
480 tx = 1
481 else:
482 liste_taux = self.devise_titulaire_anterieur.tauxchange_set.order_by('-annee')
483 if len(liste_taux) == 0:
484 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_titulaire_anterieur, self.poste.implantation))
485 tx = liste_taux[0].taux
486 return (float)(tx) * (float)(self.salaire_titulaire_anterieur)
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='+')