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