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 get_annee_pour_taux_devise(self
):
469 return self
.dossier
.contrat_date_debut
.year
471 def get_salaire_anterieur_euros(self
):
472 if self
.devise_anterieur
.code
== 'EUR':
475 liste_taux
= self
.devise_anterieur
.tauxchange_set
.order_by('-annee')
476 if len(liste_taux
) == 0:
477 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_anterieur
, self
.poste
.implantation
))
478 tx
= liste_taux
[0].taux
479 return (float)(tx
) * (float)(self
.salaire_anterieur
)
481 def get_salaire_titulaire_anterieur_euros(self
):
482 if self
.devise_titulaire_anterieur
is None:
484 if self
.devise_titulaire_anterieur
.code
== 'EUR':
487 liste_taux
= self
.devise_titulaire_anterieur
.tauxchange_set
.order_by('-annee')
488 if len(liste_taux
) == 0:
489 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_titulaire_anterieur
, self
.poste
.implantation
))
490 tx
= liste_taux
[0].taux
491 return (float)(tx
) * (float)(self
.salaire_titulaire_anterieur
)
493 def get_salaire_euros(self
):
494 tx
= self
.taux_devise()
495 return (float)(tx
) * (float)(self
.salaire
)
497 def get_remunerations_brutes(self
):
501 4 Indemnité d'expatriation
502 5 Indemnité pour frais
503 6 Indemnité de logement
504 7 Indemnité de fonction
505 8 Indemnité de responsabilité
506 9 Indemnité de transport
507 10 Indemnité compensatrice
508 11 Indemnité de subsistance
509 12 Indemnité différentielle
510 13 Prime d'installation
513 16 Indemnité de départ
514 18 Prime de 13ième mois
517 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
518 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
520 def get_charges_salariales(self
):
522 20 Charges salariales ?
525 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
527 def get_total_charges_salariales(self
):
529 for r
in self
.get_charges_salariales():
530 total
+= r
.montant_euros()
533 def get_charges_patronales(self
):
535 17 Charges patronales
538 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
540 def get_total_charges_patronales(self
):
542 for r
in self
.get_charges_patronales():
543 total
+= r
.montant_euros()
546 def get_salaire_brut(self
):
548 somme des rémuérations brutes
551 for r
in self
.get_remunerations_brutes():
552 total
+= r
.montant_euros()
555 def get_salaire_net(self
):
557 salaire brut - charges salariales
560 for r
in self
.get_charges_salariales():
561 total_charges
+= r
.montant_euros()
562 return self
.get_salaire_brut() - total_charges
564 def get_couts_auf(self
):
566 salaire net + charges patronales
569 for r
in self
.get_charges_patronales():
570 total_charges
+= r
.montant_euros()
571 return self
.get_salaire_net() + total_charges
573 def get_remunerations_tierces(self
):
577 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in (2, )]
579 def get_total_remunerations_tierces(self
):
581 for r
in self
.get_remunerations_tierces():
582 total
+= r
.montant_euros()
586 return self
.etat
in (DOSSIER_ETAT_REGION_FINALISATION
,
587 DOSSIER_ETAT_DRH_FINALISATION
,
588 DOSSIER_ETAT_FINALISE
)
591 # Tester l'enregistrement car les models.py sont importés au complet
592 if not reversion
.is_registered(Dossier
):
593 reversion
.register(Dossier
)
595 class DossierPiece(rh
.DossierPiece_
):
596 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
597 Ex.: Lettre de motivation.
601 class DossierComparaison(rh
.DossierComparaison_
):
603 Photo d'une comparaison salariale au moment de l'embauche.
605 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
='Statut', null
=True, blank
=True, )
606 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
='Classement', null
=True, blank
=True, )
611 class Remuneration(rh
.Remuneration_
):
616 class Contrat(rh
.Contrat_
):
619 # modèle de liaison entre les systèmes
621 class ImportDossier(models
.Model
):
622 dae
= models
.ForeignKey('dae.Dossier', related_name
='+')
623 rh
= models
.ForeignKey('rh.Dossier', related_name
='+')
625 class ImportPoste(models
.Model
):
626 dae
= models
.ForeignKey('dae.Poste', related_name
='+')
627 rh
= models
.ForeignKey('rh.Poste', related_name
='+')