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