prevent from no devise
[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 is None:
468 return None
469 try:
470 taux = self.taux_devise(self.devise_anterieur)
471 except Exception, e:
472 return e
473 if not taux:
474 return None
475 return int(round(float(self.salaire_anterieur) * float(taux), 2))
476
477
478 def get_salaire_titulaire_anterieur_euros(self):
479 if self.devise_titulaire_anterieur is None:
480 return None
481 try:
482 taux = self.taux_devise(self.devise_titulaire_anterieur)
483 except Exception, e:
484 return e
485 if not taux:
486 return None
487 return int(round(float(self.salaire_titulaire_anterieur) * float(taux), 2))
488
489 def get_salaire_euros(self):
490 tx = self.taux_devise()
491 return (float)(tx) * (float)(self.salaire)
492
493 def get_remunerations_brutes(self):
494 """
495 1 Salaire de base
496 3 Indemnité de base
497 4 Indemnité d'expatriation
498 5 Indemnité pour frais
499 6 Indemnité de logement
500 7 Indemnité de fonction
501 8 Indemnité de responsabilité
502 9 Indemnité de transport
503 10 Indemnité compensatrice
504 11 Indemnité de subsistance
505 12 Indemnité différentielle
506 13 Prime d'installation
507 14 Billet d'avion
508 15 Déménagement
509 16 Indemnité de départ
510 18 Prime de 13ième mois
511 19 Prime d'intérim
512 """
513 ids = [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
514 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
515
516 def get_charges_salariales(self):
517 """
518 20 Charges salariales ?
519 """
520 ids = [20, ]
521 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
522
523 def get_total_charges_salariales(self):
524 total = 0.0
525 for r in self.get_charges_salariales():
526 total += r.montant_euros()
527 return total
528
529 def get_charges_patronales(self):
530 """
531 17 Charges patronales
532 """
533 ids = [17, ]
534 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
535
536 def get_total_charges_patronales(self):
537 total = 0.0
538 for r in self.get_charges_patronales():
539 total += r.montant_euros()
540 return total
541
542 def get_salaire_brut(self):
543 """
544 somme des rémuérations brutes
545 """
546 total = 0.0
547 for r in self.get_remunerations_brutes():
548 total += r.montant_euros()
549 return total
550
551 def get_salaire_net(self):
552 """
553 salaire brut - charges salariales
554 """
555 total_charges = 0.0
556 for r in self.get_charges_salariales():
557 total_charges += r.montant_euros()
558 return self.get_salaire_brut() - total_charges
559
560 def get_couts_auf(self):
561 """
562 salaire net + charges patronales
563 """
564 total_charges = 0.0
565 for r in self.get_charges_patronales():
566 total_charges += r.montant_euros()
567 return self.get_salaire_net() + total_charges
568
569 def get_remunerations_tierces(self):
570 """
571 2 Salaire MAD
572 """
573 return [r for r in self.dae_remunerations.all() if r.type_id in (2, )]
574
575 def get_total_remunerations_tierces(self):
576 total = 0.0
577 for r in self.get_remunerations_tierces():
578 total += r.montant_euros()
579 return total
580
581 def valide(self):
582 return self.etat in (DOSSIER_ETAT_REGION_FINALISATION,
583 DOSSIER_ETAT_DRH_FINALISATION,
584 DOSSIER_ETAT_FINALISE)
585
586
587 # Tester l'enregistrement car les models.py sont importés au complet
588 if not reversion.is_registered(Dossier):
589 reversion.register(Dossier)
590
591 class DossierPiece(rh.DossierPiece_):
592 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
593 Ex.: Lettre de motivation.
594 """
595 pass
596
597 class DossierComparaison(rh.DossierComparaison_):
598 """
599 Photo d'une comparaison salariale au moment de l'embauche.
600 """
601 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name='Statut', null=True, blank=True, )
602 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name='Classement', null=True, blank=True, )
603
604
605 ### RÉMUNÉRATION
606
607 class Remuneration(rh.Remuneration_):
608 pass
609
610 ### CONTRATS
611
612 class Contrat(rh.Contrat_):
613 pass
614
615 # modèle de liaison entre les systèmes
616
617 class ImportDossier(models.Model):
618 dae = models.ForeignKey('dae.Dossier', related_name='+')
619 rh = models.ForeignKey('rh.Dossier', related_name='+')
620
621 class ImportPoste(models.Model):
622 dae = models.ForeignKey('dae.Poste', related_name='+')
623 rh = models.ForeignKey('rh.Poste', related_name='+')