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