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