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