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 from django
.db
.models
import AutoField
64 if self
.est_importe():
65 return ImportPoste
.objects
.get(dae
=self
)
67 # Faire une copie profonde de l'objet.
68 # PosteFinancement, PosteComparaison, Remun modele a ajuster...
70 def copy_model(src
, dst
, exclude
=[]):
71 keys
= [f
.name
for f
in src
._meta
.fields
if f
.name
not in ['id', ] + exclude
]
73 setattr(dst
, k
, getattr(src
, k
))
76 rh_poste
= copy_model(self
, rh_poste
)
80 for o
in self
.dae_financements
.all():
81 rh_financement
= rh
.PosteFinancement()
82 rh_financement
= copy_model(o
, rh_financement
, exclude
=['poste',])
83 rh_financement
.poste
= rh_poste
86 for o
in self
.dae_pieces
.all():
87 rh_piece
= rh
.PostePiece()
88 rh_piece
= copy_model(o
, rh_piece
, exclude
=['poste',])
89 rh_piece
.poste
= rh_poste
92 for o
in self
.dae_comparaisons_internes
.all():
93 rh_comp
= rh
.PosteComparaison()
94 rh_comp
= copy_model(o
, rh_financement
, exclude
=['poste',])
95 rh_comp
.poste
= rh_poste
102 Les vues sont montées selon une clef spéciale
103 pour identifier la provenance du poste.
104 Cette méthode fournit un moyen de reconstruire cette clef
105 afin de générer les URLs.
107 return "dae-%s" % self
.id
108 key
= property(_get_key
)
110 def get_dossiers(self
):
112 Liste tous les anciens dossiers liés à ce poste.
113 (Le nom de la relation sur le rh.Poste est mal choisi
114 poste1 au lieu de dossier1)
115 Note1 : seulement le dosssier principal fait l'objet de la recherche.
116 Note2 : les dossiers sont retournés du plus récent au plus vieux.
117 (Ce test est fait en fonction du id,
118 car les dates de création sont absentes de rh v1).
120 if self
.id_rh
is None:
122 return self
.id_rh
.rh_dossiers
.all()
125 def get_employe(self
):
127 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
129 dossiers
= self
.get_dossiers()
130 if len(dossiers
) > 0:
131 return dossiers
[0].employe
135 def get_default_devise(self
):
136 """Récupère la devise par défaut en fonction de l'implantation
140 implantation_devise
= rh
.TauxChange
.objects \
141 .filter(implantation
=self
.implantation
)[0].devise
143 implantation_devise
= 5 # EUR
144 return implantation_devise
146 #####################
147 # Classement de poste
148 #####################
150 def get_couts_minimum(self
):
151 return self
.salaire_min
+ self
.indemn_expat_min
+ self
.indemn_fct_min
+ self
.charges_patronales_min
+ self
.autre_min
153 def get_salaire_minimum(self
):
154 return self
.get_couts_minimum() - self
.charges_patronales_min
156 def get_taux_minimum(self
):
157 if self
.devise_min
.code
== 'EUR':
159 liste_taux
= self
.devise_min
.tauxchange_set
.order_by('-annee')
160 if len(liste_taux
) == 0:
161 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_min
, self
.implantation
))
163 return liste_taux
[0].taux
165 def get_couts_minimum_euros(self
):
166 return float(self
.get_couts_minimum()) * self
.get_taux_minimum()
168 def get_salaire_minimum_euros(self
):
169 return float(self
.get_salaire_minimum()) * self
.get_taux_minimum()
171 def get_couts_maximum(self
):
172 return self
.salaire_max
+ self
.indemn_expat_max
+ self
.indemn_fct_max
+ self
.charges_patronales_max
+ self
.autre_max
174 def get_salaire_maximum(self
):
175 return self
.get_couts_maximum() - self
.charges_patronales_max
177 def get_taux_maximum(self
):
178 if self
.devise_max
.code
== 'EUR':
180 liste_taux
= self
.devise_max
.tauxchange_set
.order_by('-annee')
181 if len(liste_taux
) == 0:
182 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_max
, self
.implantation
))
184 return liste_taux
[0].taux
186 def get_couts_maximum_euros(self
):
187 return float(self
.get_couts_maximum()) * self
.get_taux_maximum()
189 def get_salaire_maximum_euros(self
):
190 return float(self
.get_salaire_maximum()) * self
.get_taux_maximum()
192 def show_taux_minimum(self
):
194 return self
.get_taux_minimum()
195 except DeviseException
, e
:
198 def show_couts_minimum_euros(self
):
200 return self
.get_couts_minimum_euros()
201 except DeviseException
, e
:
204 def show_salaire_minimum_euros(self
):
206 return self
.get_salaire_minimum_euros()
207 except DeviseException
, e
:
210 def show_taux_maximum(self
):
212 return self
.get_taux_maximum()
213 except DeviseException
, e
:
216 def show_couts_maximum_euros(self
):
218 return self
.get_couts_maximum_euros()
219 except DeviseException
, e
:
222 def show_salaire_maximum_euros(self
):
224 return self
.get_salaire_maximum_euros()
225 except DeviseException
, e
:
229 ######################
230 # Comparaison de poste
231 ######################
233 def est_comparable(self
):
235 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
238 if self
.comp_universite_min
is None and \
239 self
.comp_fonctionpub_min
is None and \
240 self
.comp_locale_min
is None and \
241 self
.comp_ong_min
is None and \
242 self
.comp_autre_min
is None and \
243 self
.comp_universite_max
is None and \
244 self
.comp_fonctionpub_max
is None and \
245 self
.comp_locale_max
is None and \
246 self
.comp_ong_max
is None and \
247 self
.comp_autre_max
is None:
253 def get_taux_comparaison(self
):
255 return rh
.TauxChange
.objects
.filter(devise
=self
.devise_comparaison
)[0].taux
259 def get_comp_universite_min_euros(self
):
260 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
262 def get_comp_fonctionpub_min_euros(self
):
263 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
265 def get_comp_locale_min_euros(self
):
266 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
268 def get_comp_ong_min_euros(self
):
269 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
271 def get_comp_autre_min_euros(self
):
272 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
274 def get_comp_universite_max_euros(self
):
275 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
277 def get_comp_fonctionpub_max_euros(self
):
278 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
280 def get_comp_locale_max_euros(self
):
281 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
283 def get_comp_ong_max_euros(self
):
284 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
286 def get_comp_autre_max_euros(self
):
287 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
290 def __unicode__(self
):
292 Cette fonction est consommatrice SQL car elle cherche les dossiers
293 qui ont été liés à celui-ci.
300 return u
'%s - %s (%s)' % data
303 # Tester l'enregistrement car les models.py sont importés au complet
304 if not reversion
.is_registered(Poste
):
305 reversion
.register(Poste
)
308 POSTE_FINANCEMENT_CHOICES
= (
309 ('A', 'A - Frais de personnel'),
310 ('B', 'B - Projet(s)-Titre(s)'),
314 class PosteFinancement(rh
.PosteFinancement_
):
317 class PostePiece(rh
.PostePiece_
):
320 class PosteComparaison(rh
.PosteComparaison_
):
321 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
=u
'Statut', null
=True, blank
=True, )
322 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
=u
'Classement', null
=True, blank
=True, )
326 # TODO : migration pour m -> M, f -> F
333 class Employe(AUFMetadata
):
336 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
337 verbose_name
=u
'Employé')
338 nom
= models
.CharField(max_length
=255)
339 prenom
= models
.CharField(max_length
=255, verbose_name
=u
'Prénom')
340 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
342 def __unicode__(self
):
343 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
348 STATUT_RESIDENCE_CHOICES
= (
350 ('expat', 'Expatrié'),
353 COMPTE_COMPTA_CHOICES
= (
359 class Dossier(DossierWorkflow
, rh
.Dossier_
):
360 poste
= models
.ForeignKey('Poste', db_column
='poste', related_name
='%(app_label)s_dossiers')
361 employe
= models
.ForeignKey('Employe', db_column
='employe',
362 related_name
='%(app_label)s_dossiers',
363 verbose_name
=u
"Employé")
364 organisme_bstg_autre
= models
.CharField(max_length
=255,
365 verbose_name
=u
"Autre organisme",
366 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
370 # Données antérieures de l'employé
371 statut_anterieur
= models
.ForeignKey(
372 rh
.Statut
, related_name
='+', null
=True, blank
=True,
373 verbose_name
=u
'Statut antérieur')
374 classement_anterieur
= models
.ForeignKey(
375 rh
.Classement
, related_name
='+', null
=True, blank
=True,
376 verbose_name
=u
'Classement précédent')
377 salaire_anterieur
= models
.DecimalField(
378 max_digits
=12, decimal_places
=2, null
=True, default
=None,
379 blank
=True, verbose_name
=u
'Salaire précédent')
380 devise_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+',
381 null
=True, blank
=True)
382 type_contrat_anterieur
= models
.ForeignKey(rh
.TypeContrat
,
383 related_name
='+', null
=True, blank
=True,
384 verbose_name
=u
'Type contrat antérieur', )
386 # Données du titulaire précédent
387 employe_anterieur
= models
.ForeignKey(
388 rh
.Employe
, related_name
='+', null
=True, blank
=True,
389 verbose_name
=u
'Employé précédent')
390 statut_titulaire_anterieur
= models
.ForeignKey(
391 rh
.Statut
, related_name
='+', null
=True, blank
=True,
392 verbose_name
=u
'Statut titulaire précédent')
393 classement_titulaire_anterieur
= models
.ForeignKey(
394 rh
.Classement
, related_name
='+', null
=True, blank
=True,
395 verbose_name
=u
'Classement titulaire précédent')
396 salaire_titulaire_anterieur
= models
.DecimalField(
397 max_digits
=12, decimal_places
=2, default
=None, null
=True,
398 blank
=True, verbose_name
=u
'Salaire titulaire précédent')
399 devise_titulaire_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+', null
=True, blank
=True)
402 salaire
= models
.DecimalField(max_digits
=13, decimal_places
=2,
403 verbose_name
=u
'Salaire de base',
404 null
=True, default
=None)
405 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
408 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
409 contrat_date_debut
= models
.DateField(help_text
=HELP_TEXT_DATE
)
410 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
411 help_text
=HELP_TEXT_DATE
)
414 justif_nouveau_statut_label
= u
'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
415 justif_nouveau_statut
= models
.TextField(verbose_name
=justif_nouveau_statut_label
, null
=True, blank
=True)
416 justif_nouveau_tmp_remplacement_label
= u
"Si l'employé effectue un remplacement temporaire, préciser"
417 justif_nouveau_tmp_remplacement
= models
.TextField(verbose_name
=justif_nouveau_tmp_remplacement_label
, null
=True, blank
=True)
418 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 "
419 justif_nouveau_salaire
= models
.TextField(verbose_name
=justif_nouveau_salaire_label
, null
=True, blank
=True)
420 justif_nouveau_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
421 justif_nouveau_commentaire
= models
.TextField(verbose_name
=justif_nouveau_commentaire_label
, null
=True, blank
=True)
422 justif_rempl_type_contrat_label
= u
"Changement de type de contrat, ex : d'un CDD en CDI"
423 justif_rempl_type_contrat
= models
.TextField(verbose_name
=justif_rempl_type_contrat_label
, null
=True, blank
=True)
424 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"
425 justif_rempl_statut_employe
= models
.TextField(verbose_name
=justif_rempl_statut_employe_label
, null
=True, blank
=True)
426 justif_rempl_evaluation_label
= u
"L'évaluation de l'employé est-elle favorable? Préciser"
427 justif_rempl_evaluation
= models
.TextField(verbose_name
=justif_rempl_evaluation_label
, null
=True, blank
=True)
428 justif_rempl_salaire_label
= u
"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
429 justif_rempl_salaire
= models
.TextField(verbose_name
=justif_rempl_salaire_label
, null
=True, blank
=True)
430 justif_rempl_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
431 justif_rempl_commentaire
= models
.TextField(verbose_name
=justif_rempl_commentaire_label
, null
=True, blank
=True)
434 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
435 verbose_name
=u
'Compte comptabilité',
436 choices
=COMPTE_COMPTA_CHOICES
)
437 compte_courriel
= models
.BooleanField()
440 dae_numerisee
= models
.FileField(upload_to
='dae/dae_numerisee', storage
=UPLOAD_STORAGE
,
441 blank
=True, null
=True, verbose_name
="DAE numérisée")
444 objects
= DossierManager()
446 def __init__(self
, *args
, **kwargs
):
447 # Bouchon pour créer une date fictive necessaire pour valider un dossier
448 # à cause de l'héritage
449 super(rh
.Dossier_
, self
).__init__(*args
, **kwargs
)
450 super(DossierWorkflow
, self
).__init__(*args
, **kwargs
)
452 self
.date_debut
= datetime
.datetime
.today()
454 def __unicode__(self
):
455 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
457 def est_importe(self
):
458 """Test si le dossier a déjà été importé"""
459 return dae
.ImportDossier
.objects
.filter(dae
=self
).exists()
462 if not self
.poste
.est_importe():
463 raise Exception('Le poste de cette DAE doît être importé')
466 #def get_annee_pour_taux_devise(self):
467 # return self.contrat_date_debut.year
469 def get_salaire_anterieur_euros(self
):
470 if self
.devise_anterieur
.code
== 'EUR':
473 liste_taux
= self
.devise_anterieur
.tauxchange_set
.order_by('-annee')
474 if len(liste_taux
) == 0:
475 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_anterieur
, self
.poste
.implantation
))
476 tx
= liste_taux
[0].taux
477 return (float)(tx
) * (float)(self
.salaire_anterieur
)
479 def get_salaire_titulaire_anterieur_euros(self
):
480 if self
.devise_titulaire_anterieur
is None:
482 if self
.devise_titulaire_anterieur
.code
== 'EUR':
485 liste_taux
= self
.devise_titulaire_anterieur
.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_titulaire_anterieur
, self
.poste
.implantation
))
488 tx
= liste_taux
[0].taux
489 return (float)(tx
) * (float)(self
.salaire_titulaire_anterieur
)
491 def get_salaire_euros(self
):
492 tx
= self
.taux_devise()
493 return (float)(tx
) * (float)(self
.salaire
)
495 def get_remunerations_brutes(self
):
499 4 Indemnité d'expatriation
500 5 Indemnité pour frais
501 6 Indemnité de logement
502 7 Indemnité de fonction
503 8 Indemnité de responsabilité
504 9 Indemnité de transport
505 10 Indemnité compensatrice
506 11 Indemnité de subsistance
507 12 Indemnité différentielle
508 13 Prime d'installation
511 16 Indemnité de départ
512 18 Prime de 13ième mois
515 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
516 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
518 def get_charges_salariales(self
):
520 20 Charges salariales ?
523 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
525 def get_total_charges_salariales(self
):
527 for r
in self
.get_charges_salariales():
528 total
+= r
.montant_euros()
531 def get_charges_patronales(self
):
533 17 Charges patronales
536 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
538 def get_total_charges_patronales(self
):
540 for r
in self
.get_charges_patronales():
541 total
+= r
.montant_euros()
544 def get_salaire_brut(self
):
546 somme des rémuérations brutes
549 for r
in self
.get_remunerations_brutes():
550 total
+= r
.montant_euros()
553 def get_salaire_net(self
):
555 salaire brut - charges salariales
558 for r
in self
.get_charges_salariales():
559 total_charges
+= r
.montant_euros()
560 return self
.get_salaire_brut() - total_charges
562 def get_couts_auf(self
):
564 salaire net + charges patronales
567 for r
in self
.get_charges_patronales():
568 total_charges
+= r
.montant_euros()
569 return self
.get_salaire_net() + total_charges
571 def get_remunerations_tierces(self
):
575 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in (2, )]
577 def get_total_remunerations_tierces(self
):
579 for r
in self
.get_remunerations_tierces():
580 total
+= r
.montant_euros()
584 return self
.etat
in (DOSSIER_ETAT_REGION_FINALISATION
,
585 DOSSIER_ETAT_DRH_FINALISATION
,
586 DOSSIER_ETAT_FINALISE
)
589 # Tester l'enregistrement car les models.py sont importés au complet
590 if not reversion
.is_registered(Dossier
):
591 reversion
.register(Dossier
)
593 class DossierPiece(rh
.DossierPiece_
):
594 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
595 Ex.: Lettre de motivation.
599 class DossierComparaison(rh
.DossierComparaison_
):
601 Photo d'une comparaison salariale au moment de l'embauche.
603 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
='Statut', null
=True, blank
=True, )
604 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
='Classement', null
=True, blank
=True, )
609 class Remuneration(rh
.Remuneration_
):
614 class Contrat(rh
.Contrat_
):
617 # modèle de liaison entre les systèmes
619 class ImportDossier(models
.Model
):
620 dae
= models
.ForeignKey('dae.Dossier', related_name
='+')
621 rh
= models
.ForeignKey('rh.Dossier', related_name
='+')
623 class ImportPoste(models
.Model
):
624 dae
= models
.ForeignKey('dae.Poste', related_name
='+')
625 rh
= models
.ForeignKey('rh.Poste', related_name
='+')