1 # -=- encoding: utf-8 -=-
4 from django
.conf
import settings
5 from django
.core
.files
.storage
import FileSystemStorage
6 from django
.db
import models
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
, \
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
19 UPLOAD_STORAGE
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
)
24 POSTE_APPEL_CHOICES
= (
25 ('interne', 'Interne'),
26 ('externe', 'Externe'),
29 ('N', u
"Nouveau poste"),
30 ('M', u
"Poste existant"),
31 ('E', u
"Évolution de poste"),
35 class DeviseException(Exception):
36 silent_variable_failure
= True
39 class Poste(PosteWorkflow
, rh
.Poste_
):
41 type_intervention
= models
.CharField(max_length
=1, choices
=POSTE_ACTION
, default
='N')
44 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
46 verbose_name
=u
"Mise à jour du poste")
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)
57 objects
= PosteManager()
60 def est_importe(self
):
61 """Test si le poste a déjà été importé"""
62 return ImportPoste
.objects
.filter(dae
=self
).exists()
65 from django
.db
.models
import AutoField
66 if self
.est_importe():
67 return ImportPoste
.objects
.get(dae
=self
)
69 # Faire une copie profonde de l'objet.
70 # PosteFinancement, PosteComparaison, Remun modele a ajuster...
72 def copy_model(src
, dst
, exclude
=[]):
73 keys
= [f
.name
for f
in src
._meta
.fields
if f
.name
not in ['id', ] + exclude
]
75 setattr(dst
, k
, getattr(src
, k
))
78 rh_poste
= copy_model(self
, rh_poste
)
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
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
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
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.
109 return "dae-%s" % self
.id
110 key
= property(_get_key
)
112 def get_dossiers(self
):
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).
122 if self
.id_rh
is None:
124 return self
.id_rh
.rh_dossiers
.all()
127 def get_employe(self
):
129 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
131 dossiers
= self
.get_dossiers()
132 if len(dossiers
) > 0:
133 return dossiers
[0].employe
137 def get_default_devise(self
):
138 """Récupère la devise par défaut en fonction de l'implantation
142 implantation_devise
= rh
.TauxChange
.objects \
143 .filter(implantation
=self
.implantation
)[0].devise
145 implantation_devise
= 5 # EUR
146 return implantation_devise
148 #####################
149 # Classement de poste
150 #####################
152 def get_couts_minimum(self
):
153 return self
.salaire_min
+ self
.indemn_expat_min
+ self
.indemn_fct_min
+ self
.charges_patronales_min
+ self
.autre_min
155 def get_salaire_minimum(self
):
156 return self
.get_couts_minimum() - self
.charges_patronales_min
158 def get_taux_minimum(self
):
159 if self
.devise_min
.code
== 'EUR':
161 liste_taux
= self
.devise_min
.tauxchange_set
.order_by('-annee')
162 if len(liste_taux
) == 0:
163 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_min
, self
.implantation
))
165 return liste_taux
[0].taux
167 def get_couts_minimum_euros(self
):
168 return float(self
.get_couts_minimum()) * self
.get_taux_minimum()
170 def get_salaire_minimum_euros(self
):
171 return float(self
.get_salaire_minimum()) * self
.get_taux_minimum()
173 def get_couts_maximum(self
):
174 return self
.salaire_max
+ self
.indemn_expat_max
+ self
.indemn_fct_max
+ self
.charges_patronales_max
+ self
.autre_max
176 def get_salaire_maximum(self
):
177 return self
.get_couts_maximum() - self
.charges_patronales_max
179 def get_taux_maximum(self
):
180 if self
.devise_max
.code
== 'EUR':
182 liste_taux
= self
.devise_max
.tauxchange_set
.order_by('-annee')
183 if len(liste_taux
) == 0:
184 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_max
, self
.implantation
))
186 return liste_taux
[0].taux
188 def get_couts_maximum_euros(self
):
189 return float(self
.get_couts_maximum()) * self
.get_taux_maximum()
191 def get_salaire_maximum_euros(self
):
192 return float(self
.get_salaire_maximum()) * self
.get_taux_maximum()
194 def show_taux_minimum(self
):
196 return self
.get_taux_minimum()
197 except DeviseException
, e
:
200 def show_couts_minimum_euros(self
):
202 return self
.get_couts_minimum_euros()
203 except DeviseException
, e
:
206 def show_salaire_minimum_euros(self
):
208 return self
.get_salaire_minimum_euros()
209 except DeviseException
, e
:
212 def show_taux_maximum(self
):
214 return self
.get_taux_maximum()
215 except DeviseException
, e
:
218 def show_couts_maximum_euros(self
):
220 return self
.get_couts_maximum_euros()
221 except DeviseException
, e
:
224 def show_salaire_maximum_euros(self
):
226 return self
.get_salaire_maximum_euros()
227 except DeviseException
, e
:
231 ######################
232 # Comparaison de poste
233 ######################
235 def est_comparable(self
):
237 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
240 if self
.comp_universite_min
is None and \
241 self
.comp_fonctionpub_min
is None and \
242 self
.comp_locale_min
is None and \
243 self
.comp_ong_min
is None and \
244 self
.comp_autre_min
is None and \
245 self
.comp_universite_max
is None and \
246 self
.comp_fonctionpub_max
is None and \
247 self
.comp_locale_max
is None and \
248 self
.comp_ong_max
is None and \
249 self
.comp_autre_max
is None:
255 def get_taux_comparaison(self
):
257 return rh
.TauxChange
.objects
.filter(devise
=self
.devise_comparaison
)[0].taux
261 def get_comp_universite_min_euros(self
):
262 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
264 def get_comp_fonctionpub_min_euros(self
):
265 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
267 def get_comp_locale_min_euros(self
):
268 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
270 def get_comp_ong_min_euros(self
):
271 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
273 def get_comp_autre_min_euros(self
):
274 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
276 def get_comp_universite_max_euros(self
):
277 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
279 def get_comp_fonctionpub_max_euros(self
):
280 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
282 def get_comp_locale_max_euros(self
):
283 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
285 def get_comp_ong_max_euros(self
):
286 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
288 def get_comp_autre_max_euros(self
):
289 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
292 def __unicode__(self
):
294 Cette fonction est consommatrice SQL car elle cherche les dossiers
295 qui ont été liés à celui-ci.
302 return u
'%s - %s (%s)' % data
305 # Tester l'enregistrement car les models.py sont importés au complet
306 if not reversion
.is_registered(Poste
):
307 reversion
.register(Poste
)
310 POSTE_FINANCEMENT_CHOICES
= (
311 ('A', 'A - Frais de personnel'),
312 ('B', 'B - Projet(s)-Titre(s)'),
316 class PosteFinancement(rh
.PosteFinancement_
):
319 class PostePiece(rh
.PostePiece_
):
322 class PosteComparaison(rh
.PosteComparaison_
):
323 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
=u
'Statut', null
=True, blank
=True, )
324 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
=u
'Classement', null
=True, blank
=True, )
328 # TODO : migration pour m -> M, f -> F
335 class Employe(AUFMetadata
):
338 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
339 verbose_name
=u
'Employé')
340 nom
= models
.CharField(max_length
=255)
341 prenom
= models
.CharField(max_length
=255, verbose_name
=u
'Prénom')
342 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
344 def __unicode__(self
):
345 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
350 STATUT_RESIDENCE_CHOICES
= (
352 ('expat', 'Expatrié'),
355 COMPTE_COMPTA_CHOICES
= (
361 class Dossier(DossierWorkflow
, rh
.Dossier_
):
362 poste
= models
.ForeignKey('Poste', db_column
='poste', related_name
='%(app_label)s_dossiers')
363 employe
= models
.ForeignKey('Employe', db_column
='employe',
364 related_name
='%(app_label)s_dossiers',
365 verbose_name
=u
"Employé")
366 organisme_bstg_autre
= models
.CharField(max_length
=255,
367 verbose_name
=u
"Autre organisme",
368 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
372 # Données antérieures de l'employé
373 statut_anterieur
= models
.ForeignKey(
374 rh
.Statut
, related_name
='+', null
=True, blank
=True,
375 verbose_name
=u
'Statut antérieur')
376 classement_anterieur
= models
.ForeignKey(
377 rh
.Classement
, related_name
='+', null
=True, blank
=True,
378 verbose_name
=u
'Classement précédent')
379 salaire_anterieur
= models
.DecimalField(
380 max_digits
=12, decimal_places
=2, null
=True, default
=None,
381 blank
=True, verbose_name
=u
'Salaire précédent')
382 devise_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+',
383 null
=True, blank
=True)
384 type_contrat_anterieur
= models
.ForeignKey(rh
.TypeContrat
,
385 related_name
='+', null
=True, blank
=True,
386 verbose_name
=u
'Type contrat antérieur', )
388 # Données du titulaire précédent
389 employe_anterieur
= models
.ForeignKey(
390 rh
.Employe
, related_name
='+', null
=True, blank
=True,
391 verbose_name
=u
'Employé précédent')
392 statut_titulaire_anterieur
= models
.ForeignKey(
393 rh
.Statut
, related_name
='+', null
=True, blank
=True,
394 verbose_name
=u
'Statut titulaire précédent')
395 classement_titulaire_anterieur
= models
.ForeignKey(
396 rh
.Classement
, related_name
='+', null
=True, blank
=True,
397 verbose_name
=u
'Classement titulaire précédent')
398 salaire_titulaire_anterieur
= models
.DecimalField(
399 max_digits
=12, decimal_places
=2, default
=None, null
=True,
400 blank
=True, verbose_name
=u
'Salaire titulaire précédent')
401 devise_titulaire_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+', null
=True, blank
=True)
404 salaire
= models
.DecimalField(max_digits
=13, decimal_places
=2,
405 verbose_name
=u
'Salaire de base',
406 null
=True, default
=None)
407 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
410 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
411 contrat_date_debut
= models
.DateField(help_text
=HELP_TEXT_DATE
)
412 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
413 help_text
=HELP_TEXT_DATE
)
416 justif_nouveau_statut_label
= u
'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
417 justif_nouveau_statut
= models
.TextField(verbose_name
=justif_nouveau_statut_label
, null
=True, blank
=True)
418 justif_nouveau_tmp_remplacement_label
= u
"Si l'employé effectue un remplacement temporaire, préciser"
419 justif_nouveau_tmp_remplacement
= models
.TextField(verbose_name
=justif_nouveau_tmp_remplacement_label
, null
=True, blank
=True)
420 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 "
421 justif_nouveau_salaire
= models
.TextField(verbose_name
=justif_nouveau_salaire_label
, null
=True, blank
=True)
422 justif_nouveau_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
423 justif_nouveau_commentaire
= models
.TextField(verbose_name
=justif_nouveau_commentaire_label
, null
=True, blank
=True)
424 justif_rempl_type_contrat_label
= u
"Changement de type de contrat, ex : d'un CDD en CDI"
425 justif_rempl_type_contrat
= models
.TextField(verbose_name
=justif_rempl_type_contrat_label
, null
=True, blank
=True)
426 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"
427 justif_rempl_statut_employe
= models
.TextField(verbose_name
=justif_rempl_statut_employe_label
, null
=True, blank
=True)
428 justif_rempl_evaluation_label
= u
"L'évaluation de l'employé est-elle favorable? Préciser"
429 justif_rempl_evaluation
= models
.TextField(verbose_name
=justif_rempl_evaluation_label
, null
=True, blank
=True)
430 justif_rempl_salaire_label
= u
"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
431 justif_rempl_salaire
= models
.TextField(verbose_name
=justif_rempl_salaire_label
, null
=True, blank
=True)
432 justif_rempl_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
433 justif_rempl_commentaire
= models
.TextField(verbose_name
=justif_rempl_commentaire_label
, null
=True, blank
=True)
436 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
437 verbose_name
=u
'Compte comptabilité',
438 choices
=COMPTE_COMPTA_CHOICES
)
439 compte_courriel
= models
.BooleanField()
442 dae_numerisee
= models
.FileField(upload_to
='dae/dae_numerisee', storage
=UPLOAD_STORAGE
,
443 blank
=True, null
=True, verbose_name
="DAE numérisée")
446 objects
= DossierManager()
448 def __init__(self
, *args
, **kwargs
):
449 # Bouchon pour créer une date fictive necessaire pour valider un dossier
450 # à cause de l'héritage
451 super(rh
.Dossier_
, self
).__init__(*args
, **kwargs
)
452 super(DossierWorkflow
, self
).__init__(*args
, **kwargs
)
454 self
.date_debut
= datetime
.datetime
.today()
456 def __unicode__(self
):
457 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
459 def est_importe(self
):
460 """Test si le dossier a déjà été importé"""
461 return dae
.ImportDossier
.objects
.filter(dae
=self
).exists()
464 if not self
.poste
.est_importe():
465 raise Exception('Le poste de cette DAE doît être importé')
468 def taux_devise(self
):
469 if self
.devise
.code
== 'EUR':
471 liste_taux
= self
.devise
.tauxchange_set
.order_by('-annee')
472 if len(liste_taux
) == 0:
473 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise
, self
.poste
.implantation
))
475 return liste_taux
[0].taux
477 def get_salaire_anterieur_euros(self
):
478 if self
.devise_anterieur
.code
== 'EUR':
481 liste_taux
= self
.devise_anterieur
.tauxchange_set
.order_by('-annee')
482 if len(liste_taux
) == 0:
483 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_anterieur
, self
.poste
.implantation
))
484 tx
= liste_taux
[0].taux
485 return (float)(tx
) * (float)(self
.salaire_anterieur
)
487 def get_salaire_titulaire_anterieur_euros(self
):
488 if self
.devise_titulaire_anterieur
is None:
490 if self
.devise_titulaire_anterieur
.code
== 'EUR':
493 liste_taux
= self
.devise_titulaire_anterieur
.tauxchange_set
.order_by('-annee')
494 if len(liste_taux
) == 0:
495 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_titulaire_anterieur
, self
.poste
.implantation
))
496 tx
= liste_taux
[0].taux
497 return (float)(tx
) * (float)(self
.salaire_titulaire_anterieur
)
499 def get_salaire_euros(self
):
500 tx
= self
.taux_devise()
501 return (float)(tx
) * (float)(self
.salaire
)
503 def get_remunerations_brutes(self
):
507 4 Indemnité d'expatriation
508 5 Indemnité pour frais
509 6 Indemnité de logement
510 7 Indemnité de fonction
511 8 Indemnité de responsabilité
512 9 Indemnité de transport
513 10 Indemnité compensatrice
514 11 Indemnité de subsistance
515 12 Indemnité différentielle
516 13 Prime d'installation
519 16 Indemnité de départ
520 18 Prime de 13ième mois
523 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
524 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
526 def get_charges_salariales(self
):
528 20 Charges salariales ?
531 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
533 def get_total_charges_salariales(self
):
535 for r
in self
.get_charges_salariales():
536 total
+= r
.montant_euros()
539 def get_charges_patronales(self
):
541 17 Charges patronales
544 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
546 def get_total_charges_patronales(self
):
548 for r
in self
.get_charges_patronales():
549 total
+= r
.montant_euros()
552 def get_salaire_brut(self
):
554 somme des rémuérations brutes
557 for r
in self
.get_remunerations_brutes():
558 total
+= r
.montant_euros()
561 def get_salaire_net(self
):
563 salaire brut - charges salariales
566 for r
in self
.get_charges_salariales():
567 total_charges
+= r
.montant_euros()
568 return self
.get_salaire_brut() - total_charges
570 def get_couts_auf(self
):
572 salaire net + charges patronales
575 for r
in self
.get_charges_patronales():
576 total_charges
+= r
.montant_euros()
577 return self
.get_salaire_net() + total_charges
579 def get_remunerations_tierces(self
):
583 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in (2, )]
585 def get_total_remunerations_tierces(self
):
587 for r
in self
.get_remunerations_tierces():
588 total
+= r
.montant_euros()
592 return self
.etat
in (DOSSIER_ETAT_REGION_FINALISATION
,
593 DOSSIER_ETAT_DRH_FINALISATION
,
594 DOSSIER_ETAT_FINALISE
)
597 # Tester l'enregistrement car les models.py sont importés au complet
598 if not reversion
.is_registered(Dossier
):
599 reversion
.register(Dossier
)
601 class DossierPiece(rh
.DossierPiece_
):
602 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
603 Ex.: Lettre de motivation.
607 class DossierComparaison(rh
.DossierComparaison_
):
609 Photo d'une comparaison salariale au moment de l'embauche.
611 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
='Statut', null
=True, blank
=True, )
612 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
='Classement', null
=True, blank
=True, )
617 class Remuneration(rh
.Remuneration_
):
622 class Contrat(rh
.Contrat_
):
625 # modèle de liaison entre les systèmes
627 class ImportDossier(models
.Model
):
628 dae
= models
.ForeignKey('dae.Dossier', related_name
='+')
629 rh
= models
.ForeignKey('rh.Dossier', related_name
='+')
631 class ImportPoste(models
.Model
):
632 dae
= models
.ForeignKey('dae.Poste', related_name
='+')
633 rh
= models
.ForeignKey('rh.Poste', related_name
='+')