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