1 # -=- encoding: utf-8 -=-
3 from django
.conf
import settings
4 from django
.core
.files
.storage
import FileSystemStorage
5 from django
.db
import models
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
, \
11 from auf
.django
.metadata
.models
import AUFMetadata
12 from managers
import *
13 from rh
.models
import HELP_TEXT_DATE
17 UPLOAD_STORAGE
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
)
22 POSTE_APPEL_CHOICES
= (
23 ('interne', 'Interne'),
24 ('externe', 'Externe'),
27 ('N', u
"Nouveau poste"),
28 ('M', u
"Poste existant"),
29 ('E', u
"Évolution de poste"),
33 class DeviseException(Exception):
34 silent_variable_failure
= True
37 class Poste(PosteWorkflow
, rh
.Poste_
):
39 type_intervention
= models
.CharField(max_length
=1, choices
=POSTE_ACTION
, default
='N')
42 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
44 verbose_name
=u
"Mise à jour du poste")
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)
55 objects
= PosteManager()
58 def est_importe(self
):
59 """Test si le poste a déjà été importé"""
60 return ImportPoste
.objects
.filter(dae
=self
).exists()
63 if self
.est_importe():
64 return ImportPoste
.objects
.get(dae
=self
)
66 # Faire une copie profonde de l'objet.
67 # PosteFinancement, PosteComparaison, Remun modele a ajuster...
69 def copy_model(src
, dst
, exclude
=[]):
70 keys
= [f
.name
for f
in src
._meta
.fields
if f
.name
not in ['id', ] + exclude
]
72 setattr(dst
, k
, getattr(src
, k
))
75 rh_poste
= copy_model(self
, rh_poste
)
79 for o
in self
.dae_financements
.all():
80 rh_financement
= rh
.PosteFinancement()
81 rh_financement
= copy_model(o
, rh_financement
, exclude
=['poste',])
82 rh_financement
.poste
= rh_poste
85 for o
in self
.dae_pieces
.all():
86 rh_piece
= rh
.PostePiece()
87 rh_piece
= copy_model(o
, rh_piece
, exclude
=['poste',])
88 rh_piece
.poste
= rh_poste
91 for o
in self
.dae_comparaisons_internes
.all():
92 rh_comp
= rh
.PosteComparaison()
93 rh_comp
= copy_model(o
, rh_financement
, exclude
=['poste',])
94 rh_comp
.poste
= rh_poste
101 Les vues sont montées selon une clef spéciale
102 pour identifier la provenance du poste.
103 Cette méthode fournit un moyen de reconstruire cette clef
104 afin de générer les URLs.
106 return "dae-%s" % self
.id
107 key
= property(_get_key
)
109 def get_dossiers(self
):
111 Liste tous les anciens dossiers liés à ce poste.
112 (Le nom de la relation sur le rh.Poste est mal choisi
113 poste1 au lieu de dossier1)
114 Note1 : seulement le dosssier principal fait l'objet de la recherche.
115 Note2 : les dossiers sont retournés du plus récent au plus vieux.
116 (Ce test est fait en fonction du id,
117 car les dates de création sont absentes de rh v1).
119 if self
.id_rh
is None:
121 return self
.id_rh
.rh_dossiers
.all()
124 def get_employe(self
):
126 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
128 dossiers
= self
.get_dossiers()
129 if len(dossiers
) > 0:
130 return dossiers
[0].employe
134 def get_default_devise(self
):
135 """Récupère la devise par défaut en fonction de l'implantation
139 implantation_devise
= rh
.TauxChange
.objects \
140 .filter(implantation
=self
.implantation
)[0].devise
142 implantation_devise
= 5 # EUR
143 return implantation_devise
145 #####################
146 # Classement de poste
147 #####################
149 def get_couts_minimum(self
):
150 return self
.salaire_min
+ self
.indemn_expat_min
+ self
.indemn_fct_min
+ self
.charges_patronales_min
+ self
.autre_min
152 def get_salaire_minimum(self
):
153 return self
.get_couts_minimum() - self
.charges_patronales_min
155 def get_taux_minimum(self
):
156 if self
.devise_min
.code
== 'EUR':
158 liste_taux
= self
.devise_min
.tauxchange_set
.order_by('-annee')
159 if len(liste_taux
) == 0:
160 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_min
, self
.implantation
))
162 return liste_taux
[0].taux
164 def get_couts_minimum_euros(self
):
165 return float(self
.get_couts_minimum()) * self
.get_taux_minimum()
167 def get_salaire_minimum_euros(self
):
168 return float(self
.get_salaire_minimum()) * self
.get_taux_minimum()
170 def get_couts_maximum(self
):
171 return self
.salaire_max
+ self
.indemn_expat_max
+ self
.indemn_fct_max
+ self
.charges_patronales_max
+ self
.autre_max
173 def get_salaire_maximum(self
):
174 return self
.get_couts_maximum() - self
.charges_patronales_max
176 def get_taux_maximum(self
):
177 if self
.devise_max
.code
== 'EUR':
179 liste_taux
= self
.devise_max
.tauxchange_set
.order_by('-annee')
180 if len(liste_taux
) == 0:
181 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_max
, self
.implantation
))
183 return liste_taux
[0].taux
185 def get_couts_maximum_euros(self
):
186 return float(self
.get_couts_maximum()) * self
.get_taux_maximum()
188 def get_salaire_maximum_euros(self
):
189 return float(self
.get_salaire_maximum()) * self
.get_taux_maximum()
191 def show_taux_minimum(self
):
193 return self
.get_taux_minimum()
194 except DeviseException
, e
:
197 def show_couts_minimum_euros(self
):
199 return self
.get_couts_minimum_euros()
200 except DeviseException
, e
:
203 def show_salaire_minimum_euros(self
):
205 return self
.get_salaire_minimum_euros()
206 except DeviseException
, e
:
209 def show_taux_maximum(self
):
211 return self
.get_taux_maximum()
212 except DeviseException
, e
:
215 def show_couts_maximum_euros(self
):
217 return self
.get_couts_maximum_euros()
218 except DeviseException
, e
:
221 def show_salaire_maximum_euros(self
):
223 return self
.get_salaire_maximum_euros()
224 except DeviseException
, e
:
228 ######################
229 # Comparaison de poste
230 ######################
232 def est_comparable(self
):
234 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
237 if self
.comp_universite_min
is None and \
238 self
.comp_fonctionpub_min
is None and \
239 self
.comp_locale_min
is None and \
240 self
.comp_ong_min
is None and \
241 self
.comp_autre_min
is None and \
242 self
.comp_universite_max
is None and \
243 self
.comp_fonctionpub_max
is None and \
244 self
.comp_locale_max
is None and \
245 self
.comp_ong_max
is None and \
246 self
.comp_autre_max
is None:
252 def get_taux_comparaison(self
):
254 return rh
.TauxChange
.objects
.filter(devise
=self
.devise_comparaison
)[0].taux
258 def get_comp_universite_min_euros(self
):
259 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
261 def get_comp_fonctionpub_min_euros(self
):
262 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
264 def get_comp_locale_min_euros(self
):
265 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
267 def get_comp_ong_min_euros(self
):
268 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
270 def get_comp_autre_min_euros(self
):
271 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
273 def get_comp_universite_max_euros(self
):
274 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
276 def get_comp_fonctionpub_max_euros(self
):
277 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
279 def get_comp_locale_max_euros(self
):
280 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
282 def get_comp_ong_max_euros(self
):
283 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
285 def get_comp_autre_max_euros(self
):
286 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
289 def __unicode__(self
):
291 Cette fonction est consommatrice SQL car elle cherche les dossiers
292 qui ont été liés à celui-ci.
299 return u
'%s - %s (%s)' % data
302 # Tester l'enregistrement car les models.py sont importés au complet
303 if not reversion
.is_registered(Poste
):
304 reversion
.register(Poste
)
307 POSTE_FINANCEMENT_CHOICES
= (
308 ('A', 'A - Frais de personnel'),
309 ('B', 'B - Projet(s)-Titre(s)'),
313 class PosteFinancement(rh
.PosteFinancement_
):
316 class PostePiece(rh
.PostePiece_
):
319 class PosteComparaison(rh
.PosteComparaison_
):
320 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
=u
'Statut', null
=True, blank
=True, )
321 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
=u
'Classement', null
=True, blank
=True, )
325 # TODO : migration pour m -> M, f -> F
332 class Employe(AUFMetadata
):
335 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
336 verbose_name
=u
'Employé')
337 nom
= models
.CharField(max_length
=255)
338 prenom
= models
.CharField(max_length
=255, verbose_name
=u
'Prénom')
339 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
341 def __unicode__(self
):
342 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
347 STATUT_RESIDENCE_CHOICES
= (
349 ('expat', 'Expatrié'),
352 COMPTE_COMPTA_CHOICES
= (
358 class Dossier(DossierWorkflow
, rh
.Dossier_
):
359 poste
= models
.ForeignKey('Poste', db_column
='poste', related_name
='%(app_label)s_dossiers')
360 employe
= models
.ForeignKey('Employe', db_column
='employe',
361 related_name
='%(app_label)s_dossiers',
362 verbose_name
=u
"Employé")
363 organisme_bstg_autre
= models
.CharField(max_length
=255,
364 verbose_name
=u
"Autre organisme",
365 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
369 # Données antérieures de l'employé
370 statut_anterieur
= models
.ForeignKey(
371 rh
.Statut
, related_name
='+', null
=True, blank
=True,
372 verbose_name
=u
'Statut antérieur')
373 classement_anterieur
= models
.ForeignKey(
374 rh
.Classement
, related_name
='+', null
=True, blank
=True,
375 verbose_name
=u
'Classement précédent')
376 salaire_anterieur
= models
.DecimalField(
377 max_digits
=12, decimal_places
=2, null
=True, default
=None,
378 blank
=True, verbose_name
=u
'Salaire précédent')
379 devise_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+',
380 null
=True, blank
=True)
381 type_contrat_anterieur
= models
.ForeignKey(rh
.TypeContrat
,
382 related_name
='+', null
=True, blank
=True,
383 verbose_name
=u
'Type contrat antérieur', )
385 # Données du titulaire précédent
386 employe_anterieur
= models
.ForeignKey(
387 rh
.Employe
, related_name
='+', null
=True, blank
=True,
388 verbose_name
=u
'Employé précédent')
389 statut_titulaire_anterieur
= models
.ForeignKey(
390 rh
.Statut
, related_name
='+', null
=True, blank
=True,
391 verbose_name
=u
'Statut titulaire précédent')
392 classement_titulaire_anterieur
= models
.ForeignKey(
393 rh
.Classement
, related_name
='+', null
=True, blank
=True,
394 verbose_name
=u
'Classement titulaire précédent')
395 salaire_titulaire_anterieur
= models
.DecimalField(
396 max_digits
=12, decimal_places
=2, default
=None, null
=True,
397 blank
=True, verbose_name
=u
'Salaire titulaire précédent')
398 devise_titulaire_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+', null
=True, blank
=True)
401 salaire
= models
.DecimalField(max_digits
=13, decimal_places
=2,
402 verbose_name
=u
'Salaire de base',
403 null
=True, default
=None)
404 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
407 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
408 contrat_date_debut
= models
.DateField(help_text
=HELP_TEXT_DATE
)
409 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
410 help_text
=HELP_TEXT_DATE
)
413 justif_nouveau_statut_label
= u
'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
414 justif_nouveau_statut
= models
.TextField(verbose_name
=justif_nouveau_statut_label
, null
=True, blank
=True)
415 justif_nouveau_tmp_remplacement_label
= u
"Si l'employé effectue un remplacement temporaire, préciser"
416 justif_nouveau_tmp_remplacement
= models
.TextField(verbose_name
=justif_nouveau_tmp_remplacement_label
, null
=True, blank
=True)
417 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 "
418 justif_nouveau_salaire
= models
.TextField(verbose_name
=justif_nouveau_salaire_label
, null
=True, blank
=True)
419 justif_nouveau_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
420 justif_nouveau_commentaire
= models
.TextField(verbose_name
=justif_nouveau_commentaire_label
, null
=True, blank
=True)
421 justif_rempl_type_contrat_label
= u
"Changement de type de contrat, ex : d'un CDD en CDI"
422 justif_rempl_type_contrat
= models
.TextField(verbose_name
=justif_rempl_type_contrat_label
, null
=True, blank
=True)
423 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"
424 justif_rempl_statut_employe
= models
.TextField(verbose_name
=justif_rempl_statut_employe_label
, null
=True, blank
=True)
425 justif_rempl_evaluation_label
= u
"L'évaluation de l'employé est-elle favorable? Préciser"
426 justif_rempl_evaluation
= models
.TextField(verbose_name
=justif_rempl_evaluation_label
, null
=True, blank
=True)
427 justif_rempl_salaire_label
= u
"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
428 justif_rempl_salaire
= models
.TextField(verbose_name
=justif_rempl_salaire_label
, null
=True, blank
=True)
429 justif_rempl_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
430 justif_rempl_commentaire
= models
.TextField(verbose_name
=justif_rempl_commentaire_label
, null
=True, blank
=True)
433 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
434 verbose_name
=u
'Compte comptabilité',
435 choices
=COMPTE_COMPTA_CHOICES
)
436 compte_courriel
= models
.BooleanField()
439 dae_numerisee
= models
.FileField(upload_to
='dae/dae_numerisee', storage
=UPLOAD_STORAGE
,
440 blank
=True, null
=True, verbose_name
="DAE numérisée")
443 objects
= DossierManager()
445 def __init__(self
, *args
, **kwargs
):
446 # Bouchon pour créer une date fictive necessaire pour valider un dossier
447 # à cause de l'héritage
448 super(rh
.Dossier_
, self
).__init__(*args
, **kwargs
)
449 super(DossierWorkflow
, self
).__init__(*args
, **kwargs
)
451 self
.date_debut
= datetime
.datetime
.today()
453 def __unicode__(self
):
454 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
456 def est_importe(self
):
457 """Test si le dossier a déjà été importé"""
458 return dae
.ImportDossier
.objects
.filter(dae
=self
).exists()
461 if not self
.poste
.est_importe():
462 raise Exception('Le poste de cette DAE doît être importé')
465 def get_salaire_anterieur_euros(self
):
466 if self
.devise_anterieur
is None:
469 taux
= self
.taux_devise(self
.devise_anterieur
)
474 return int(round(float(self
.salaire_anterieur
) * float(taux
), 2))
477 def get_salaire_titulaire_anterieur_euros(self
):
478 if self
.devise_titulaire_anterieur
is None:
481 taux
= self
.taux_devise(self
.devise_titulaire_anterieur
)
486 return int(round(float(self
.salaire_titulaire_anterieur
) * float(taux
), 2))
488 def get_salaire_euros(self
):
489 tx
= self
.taux_devise()
490 return (float)(tx
) * (float)(self
.salaire
)
492 def get_remunerations_brutes(self
):
496 4 Indemnité d'expatriation
497 5 Indemnité pour frais
498 6 Indemnité de logement
499 7 Indemnité de fonction
500 8 Indemnité de responsabilité
501 9 Indemnité de transport
502 10 Indemnité compensatrice
503 11 Indemnité de subsistance
504 12 Indemnité différentielle
505 13 Prime d'installation
508 16 Indemnité de départ
509 18 Prime de 13ième mois
512 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
513 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
515 def get_charges_salariales(self
):
517 20 Charges salariales ?
520 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
522 def get_total_charges_salariales(self
):
524 for r
in self
.get_charges_salariales():
525 total
+= r
.montant_euros()
528 def get_charges_patronales(self
):
530 17 Charges patronales
533 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
535 def get_total_charges_patronales(self
):
537 for r
in self
.get_charges_patronales():
538 total
+= r
.montant_euros()
541 def get_salaire_brut(self
):
543 somme des rémuérations brutes
546 for r
in self
.get_remunerations_brutes():
547 total
+= r
.montant_euros()
550 def get_salaire_net(self
):
552 salaire brut - charges salariales
555 for r
in self
.get_charges_salariales():
556 total_charges
+= r
.montant_euros()
557 return self
.get_salaire_brut() - total_charges
559 def get_couts_auf(self
):
561 salaire net + charges patronales
564 for r
in self
.get_charges_patronales():
565 total_charges
+= r
.montant_euros()
566 return self
.get_salaire_net() + total_charges
568 def get_remunerations_tierces(self
):
572 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in (2, )]
574 def get_total_remunerations_tierces(self
):
576 for r
in self
.get_remunerations_tierces():
577 total
+= r
.montant_euros()
581 return self
.etat
in (DOSSIER_ETAT_REGION_FINALISATION
,
582 DOSSIER_ETAT_DRH_FINALISATION
,
583 DOSSIER_ETAT_FINALISE
)
586 # Tester l'enregistrement car les models.py sont importés au complet
587 if not reversion
.is_registered(Dossier
):
588 reversion
.register(Dossier
)
590 class DossierPiece(rh
.DossierPiece_
):
591 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
592 Ex.: Lettre de motivation.
596 class DossierComparaison(rh
.DossierComparaison_
):
598 Photo d'une comparaison salariale au moment de l'embauche.
600 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
='Statut', null
=True, blank
=True, )
601 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
='Classement', null
=True, blank
=True, )
606 class Remuneration(rh
.Remuneration_
):
611 class Contrat(rh
.Contrat_
):
614 # modèle de liaison entre les systèmes
616 class ImportDossier(models
.Model
):
617 dae
= models
.ForeignKey('dae.Dossier', related_name
='+')
618 rh
= models
.ForeignKey('rh.Dossier', related_name
='+')
620 class ImportPoste(models
.Model
):
621 dae
= models
.ForeignKey('dae.Poste', related_name
='+')
622 rh
= models
.ForeignKey('rh.Poste', related_name
='+')