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 managers
import *
17 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
18 REGIME_TRAVAIL_DEFAULT
=100.00
19 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
=35.00
22 UPLOAD_STORAGE
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
)
27 POSTE_APPEL_CHOICES
= (
28 ('interne', 'Interne'),
29 ('externe', 'Externe'),
32 ('N', u
"Nouveau poste"),
33 ('M', u
"Poste existant"),
34 ('E', u
"Évolution de poste"),
38 class DeviseException(Exception):
39 silent_variable_failure
= True
42 class Poste(PosteWorkflow
, rh
.Poste_
):
44 type_intervention
= models
.CharField(max_length
=1, choices
=POSTE_ACTION
, default
='N')
47 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
49 verbose_name
=u
"Mise à jour du poste")
52 indemn_expat_min
= models
.DecimalField(max_digits
=13, decimal_places
=2, default
=0)
53 indemn_expat_max
= models
.DecimalField(max_digits
=12, decimal_places
=2, default
=0)
54 indemn_fct_min
= models
.DecimalField(max_digits
=12, decimal_places
=2, default
=0)
55 indemn_fct_max
= models
.DecimalField(max_digits
=12, decimal_places
=2, default
=0)
56 charges_patronales_min
= models
.DecimalField(max_digits
=12, decimal_places
=2, default
=0)
57 charges_patronales_max
= models
.DecimalField(max_digits
=12, decimal_places
=2, default
=0)
60 objects
= PosteManager()
63 def est_importe(self
):
64 """Test si le poste a déjà été importé"""
65 return ImportPoste
.objects
.filter(dae
=self
).exists()
68 from django
.db
.models
import AutoField
69 if self
.est_importe():
70 return ImportPoste
.objects
.get(dae
=self
)
72 # Faire une copie profonde de l'objet.
73 # PosteFinancement, PosteComparaison, Remun modele a ajuster...
75 def copy_model(src
, dst
, exclude
=[]):
76 keys
= [f
.name
for f
in src
._meta
.fields
if f
.name
not in ['id', ] + exclude
]
78 setattr(dst
, k
, getattr(src
, k
))
81 rh_poste
= copy_model(self
, rh_poste
)
85 for o
in self
.dae_financements
.all():
86 rh_financement
= rh
.PosteFinancement()
87 rh_financement
= copy_model(o
, rh_financement
, exclude
=['poste',])
88 rh_financement
.poste
= rh_poste
91 for o
in self
.dae_pieces
.all():
92 rh_piece
= rh
.PostePiece()
93 rh_piece
= copy_model(o
, rh_piece
, exclude
=['poste',])
94 rh_piece
.poste
= rh_poste
97 for o
in self
.dae_comparaisons_internes
.all():
98 rh_comp
= rh
.PosteComparaison()
99 rh_comp
= copy_model(o
, rh_financement
, exclude
=['poste',])
100 rh_comp
.poste
= rh_poste
107 Les vues sont montées selon une clef spéciale
108 pour identifier la provenance du poste.
109 Cette méthode fournit un moyen de reconstruire cette clef
110 afin de générer les URLs.
112 return "dae-%s" % self
.id
113 key
= property(_get_key
)
115 def get_dossiers(self
):
117 Liste tous les anciens dossiers liés à ce poste.
118 (Le nom de la relation sur le rh.Poste est mal choisi
119 poste1 au lieu de dossier1)
120 Note1 : seulement le dosssier principal fait l'objet de la recherche.
121 Note2 : les dossiers sont retournés du plus récent au plus vieux.
122 (Ce test est fait en fonction du id,
123 car les dates de création sont absentes de rh v1).
125 if self
.id_rh
is None:
127 return self
.id_rh
.rh_dossiers
.all()
129 def get_complement_nom(self
):
131 Inspecte les modèles rh v1 pour trouver dans le dernier dossier
132 un complément de titre de poste.
134 dossiers
= self
.get_dossiers()
135 if len(dossiers
) > 0:
136 nom
= dossiers
[0].poste
.nom
141 def get_employe(self
):
143 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
145 dossiers
= self
.get_dossiers()
146 if len(dossiers
) > 0:
147 return dossiers
[0].employe
151 def get_default_devise(self
):
152 """Récupère la devise par défaut en fonction de l'implantation
156 implantation_devise
= rh
.TauxChange
.objects \
157 .filter(implantation
=self
.implantation
)[0].devise
159 implantation_devise
= 5 # EUR
160 return implantation_devise
162 #####################
163 # Classement de poste
164 #####################
166 def get_couts_minimum(self
):
167 return self
.salaire_min
+ self
.indemn_expat_min
+ self
.indemn_fct_min
+ self
.charges_patronales_min
+ self
.autre_min
169 def get_salaire_minimum(self
):
170 return self
.get_couts_minimum() - self
.charges_patronales_min
172 def get_taux_minimum(self
):
173 if self
.devise_min
.code
== 'EUR':
175 liste_taux
= self
.devise_min
.tauxchange_set
.order_by('-annee')
176 if len(liste_taux
) == 0:
177 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_min
, self
.implantation
))
179 return liste_taux
[0].taux
181 def get_couts_minimum_euros(self
):
182 return float(self
.get_couts_minimum()) * self
.get_taux_minimum()
184 def get_salaire_minimum_euros(self
):
185 return float(self
.get_salaire_minimum()) * self
.get_taux_minimum()
187 def get_couts_maximum(self
):
188 return self
.salaire_max
+ self
.indemn_expat_max
+ self
.indemn_fct_max
+ self
.charges_patronales_max
+ self
.autre_max
190 def get_salaire_maximum(self
):
191 return self
.get_couts_maximum() - self
.charges_patronales_max
193 def get_taux_maximum(self
):
194 if self
.devise_max
.code
== 'EUR':
196 liste_taux
= self
.devise_max
.tauxchange_set
.order_by('-annee')
197 if len(liste_taux
) == 0:
198 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_max
, self
.implantation
))
200 return liste_taux
[0].taux
202 def get_couts_maximum_euros(self
):
203 return float(self
.get_couts_maximum()) * self
.get_taux_maximum()
205 def get_salaire_maximum_euros(self
):
206 return float(self
.get_salaire_maximum()) * self
.get_taux_maximum()
208 def show_taux_minimum(self
):
210 return self
.get_taux_minimum()
211 except DeviseException
, e
:
214 def show_couts_minimum_euros(self
):
216 return self
.get_couts_minimum_euros()
217 except DeviseException
, e
:
220 def show_salaire_minimum_euros(self
):
222 return self
.get_salaire_minimum_euros()
223 except DeviseException
, e
:
226 def show_taux_maximum(self
):
228 return self
.get_taux_maximum()
229 except DeviseException
, e
:
232 def show_couts_maximum_euros(self
):
234 return self
.get_couts_maximum_euros()
235 except DeviseException
, e
:
238 def show_salaire_maximum_euros(self
):
240 return self
.get_salaire_maximum_euros()
241 except DeviseException
, e
:
245 ######################
246 # Comparaison de poste
247 ######################
249 def est_comparable(self
):
251 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
254 if self
.comp_universite_min
is None and \
255 self
.comp_fonctionpub_min
is None and \
256 self
.comp_locale_min
is None and \
257 self
.comp_ong_min
is None and \
258 self
.comp_autre_min
is None and \
259 self
.comp_universite_max
is None and \
260 self
.comp_fonctionpub_max
is None and \
261 self
.comp_locale_max
is None and \
262 self
.comp_ong_max
is None and \
263 self
.comp_autre_max
is None:
269 def get_taux_comparaison(self
):
271 return rh
.TauxChange
.objects
.filter(devise
=self
.devise_comparaison
)[0].taux
275 def get_comp_universite_min_euros(self
):
276 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
278 def get_comp_fonctionpub_min_euros(self
):
279 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
281 def get_comp_locale_min_euros(self
):
282 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
284 def get_comp_ong_min_euros(self
):
285 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
287 def get_comp_autre_min_euros(self
):
288 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
290 def get_comp_universite_max_euros(self
):
291 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
293 def get_comp_fonctionpub_max_euros(self
):
294 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
296 def get_comp_locale_max_euros(self
):
297 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
299 def get_comp_ong_max_euros(self
):
300 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
302 def get_comp_autre_max_euros(self
):
303 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
306 def __unicode__(self
):
308 Cette fonction est consommatrice SQL car elle cherche les dossiers
309 qui ont été liés à celui-ci.
311 complement_nom_poste
= self
.get_complement_nom()
312 if complement_nom_poste
is None:
313 complement_nom_poste
= ""
319 return u
'%s - %s (%s)' % data
322 # Tester l'enregistrement car les models.py sont importés au complet
323 if not reversion
.is_registered(Poste
):
324 reversion
.register(Poste
)
327 POSTE_FINANCEMENT_CHOICES
= (
328 ('A', 'A - Frais de personnel'),
329 ('B', 'B - Projet(s)-Titre(s)'),
333 class PosteFinancement(rh
.PosteFinancement_
):
336 class PostePiece(rh
.PostePiece_
):
339 class PosteComparaison(rh
.PosteComparaison_
):
340 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
=u
'Statut', null
=True, blank
=True, )
341 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
=u
'Classement', null
=True, blank
=True, )
345 # TODO : migration pour m -> M, f -> F
352 class Employe(models
.Model
):
355 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
356 verbose_name
=u
'Employé')
357 nom
= models
.CharField(max_length
=255)
358 prenom
= models
.CharField(max_length
=255, verbose_name
=u
'Prénom')
359 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
361 def __unicode__(self
):
362 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
367 STATUT_RESIDENCE_CHOICES
= (
369 ('expat', 'Expatrié'),
372 COMPTE_COMPTA_CHOICES
= (
378 class Dossier(DossierWorkflow
, rh
.Dossier_
):
379 poste
= models
.ForeignKey('Poste', db_column
='poste', related_name
='%(app_label)s_dossiers')
380 employe
= models
.ForeignKey('Employe', db_column
='employe',
381 related_name
='%(app_label)s_dossiers',
382 verbose_name
=u
"Employé")
383 organisme_bstg_autre
= models
.CharField(max_length
=255,
384 verbose_name
=u
"Autre organisme",
385 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
389 # Données antérieures de l'employé
390 statut_anterieur
= models
.ForeignKey(
391 rh
.Statut
, related_name
='+', null
=True, blank
=True,
392 verbose_name
=u
'Statut antérieur')
393 classement_anterieur
= models
.ForeignKey(
394 rh
.Classement
, related_name
='+', null
=True, blank
=True,
395 verbose_name
=u
'Classement précédent')
396 salaire_anterieur
= models
.DecimalField(
397 max_digits
=12, decimal_places
=2, null
=True, default
=None,
398 blank
=True, verbose_name
=u
'Salaire précédent')
399 devise_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+',
400 null
=True, blank
=True)
401 type_contrat_anterieur
= models
.ForeignKey(rh
.TypeContrat
,
402 related_name
='+', null
=True, blank
=True,
403 verbose_name
=u
'Type contrat antérieur', )
405 # Données du titulaire précédent
406 employe_anterieur
= models
.ForeignKey(
407 rh
.Employe
, related_name
='+', null
=True, blank
=True,
408 verbose_name
=u
'Employé précédent')
409 statut_titulaire_anterieur
= models
.ForeignKey(
410 rh
.Statut
, related_name
='+', null
=True, blank
=True,
411 verbose_name
=u
'Statut titulaire précédent')
412 classement_titulaire_anterieur
= models
.ForeignKey(
413 rh
.Classement
, related_name
='+', null
=True, blank
=True,
414 verbose_name
=u
'Classement titulaire précédent')
415 salaire_titulaire_anterieur
= models
.DecimalField(
416 max_digits
=12, decimal_places
=2, default
=None, null
=True,
417 blank
=True, verbose_name
=u
'Salaire titulaire précédent')
418 devise_titulaire_anterieur
= models
.ForeignKey(rh
.Devise
, related_name
='+', null
=True, blank
=True)
421 salaire
= models
.DecimalField(max_digits
=13, decimal_places
=2,
422 verbose_name
=u
'Salaire de base',
423 null
=True, default
=None)
424 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
427 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
428 contrat_date_debut
= models
.DateField(help_text
=HELP_TEXT_DATE
)
429 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
430 help_text
=HELP_TEXT_DATE
)
433 justif_nouveau_statut_label
= u
'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
434 justif_nouveau_statut
= models
.TextField(verbose_name
=justif_nouveau_statut_label
, null
=True, blank
=True)
435 justif_nouveau_tmp_remplacement_label
= u
"Si l'employé effectue un remplacement temporaire, préciser"
436 justif_nouveau_tmp_remplacement
= models
.TextField(verbose_name
=justif_nouveau_tmp_remplacement_label
, null
=True, blank
=True)
437 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 "
438 justif_nouveau_salaire
= models
.TextField(verbose_name
=justif_nouveau_salaire_label
, null
=True, blank
=True)
439 justif_nouveau_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
440 justif_nouveau_commentaire
= models
.TextField(verbose_name
=justif_nouveau_commentaire_label
, null
=True, blank
=True)
441 justif_rempl_type_contrat_label
= u
"Changement de type de contrat, ex : d'un CDD en CDI"
442 justif_rempl_type_contrat
= models
.TextField(verbose_name
=justif_rempl_type_contrat_label
, null
=True, blank
=True)
443 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"
444 justif_rempl_statut_employe
= models
.TextField(verbose_name
=justif_rempl_statut_employe_label
, null
=True, blank
=True)
445 justif_rempl_evaluation_label
= u
"L'évaluation de l'employé est-elle favorable? Préciser"
446 justif_rempl_evaluation
= models
.TextField(verbose_name
=justif_rempl_evaluation_label
, null
=True, blank
=True)
447 justif_rempl_salaire_label
= u
"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
448 justif_rempl_salaire
= models
.TextField(verbose_name
=justif_rempl_salaire_label
, null
=True, blank
=True)
449 justif_rempl_commentaire_label
= u
"COMMENTAIRES ADDITIONNELS"
450 justif_rempl_commentaire
= models
.TextField(verbose_name
=justif_rempl_commentaire_label
, null
=True, blank
=True)
453 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
454 verbose_name
=u
'Compte comptabilité',
455 choices
=COMPTE_COMPTA_CHOICES
)
456 compte_courriel
= models
.BooleanField()
459 dae_numerisee
= models
.FileField(upload_to
='dae/dae_numerisee', storage
=UPLOAD_STORAGE
,
460 blank
=True, null
=True, verbose_name
="DAE numérisée")
463 objects
= DossierManager()
465 def __init__(self
, *args
, **kwargs
):
466 # Bouchon pour créer une date fictive necessaire pour valider un dossier
467 # à cause de l'héritage
468 super(rh
.Dossier_
, self
).__init__(*args
, **kwargs
)
469 super(DossierWorkflow
, self
).__init__(*args
, **kwargs
)
471 self
.date_debut
= datetime
.datetime
.today()
473 def __unicode__(self
):
474 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
476 def est_importe(self
):
477 """Test si le dossier a déjà été importé"""
478 return dae
.ImportDossier
.objects
.filter(dae
=self
).exists()
481 if not self
.poste
.est_importe():
482 raise Exception('Le poste de cette DAE doît être importé')
485 def taux_devise(self
):
486 if self
.devise
.code
== 'EUR':
488 liste_taux
= self
.devise
.tauxchange_set
.order_by('-annee')
489 if len(liste_taux
) == 0:
490 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise
, self
.poste
.implantation
))
492 return liste_taux
[0].taux
494 def get_salaire_anterieur_euros(self
):
495 if self
.devise_anterieur
.code
== 'EUR':
498 liste_taux
= self
.devise_anterieur
.tauxchange_set
.order_by('-annee')
499 if len(liste_taux
) == 0:
500 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_anterieur
, self
.poste
.implantation
))
501 tx
= liste_taux
[0].taux
502 return (float)(tx
) * (float)(self
.salaire_anterieur
)
504 def get_salaire_titulaire_anterieur_euros(self
):
505 if self
.devise_titulaire_anterieur
is None:
507 if self
.devise_titulaire_anterieur
.code
== 'EUR':
510 liste_taux
= self
.devise_titulaire_anterieur
.tauxchange_set
.order_by('-annee')
511 if len(liste_taux
) == 0:
512 raise DeviseException(u
"La devise %s n'a pas de taux pour l'implantation %s" % (self
.devise_titulaire_anterieur
, self
.poste
.implantation
))
513 tx
= liste_taux
[0].taux
514 return (float)(tx
) * (float)(self
.salaire_titulaire_anterieur
)
516 def get_salaire_euros(self
):
517 tx
= self
.taux_devise()
518 return (float)(tx
) * (float)(self
.salaire
)
520 def get_remunerations_brutes(self
):
524 4 Indemnité d'expatriation
525 5 Indemnité pour frais
526 6 Indemnité de logement
527 7 Indemnité de fonction
528 8 Indemnité de responsabilité
529 9 Indemnité de transport
530 10 Indemnité compensatrice
531 11 Indemnité de subsistance
532 12 Indemnité différentielle
533 13 Prime d'installation
536 16 Indemnité de départ
537 18 Prime de 13ième mois
540 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
541 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
543 def get_charges_salariales(self
):
545 20 Charges salariales ?
548 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
550 def get_total_charges_salariales(self
):
552 for r
in self
.get_charges_salariales():
553 total
+= r
.montant_euro()
556 def get_charges_patronales(self
):
558 17 Charges patronales
561 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in ids
]
563 def get_total_charges_patronales(self
):
565 for r
in self
.get_charges_patronales():
566 total
+= r
.montant_euro()
569 def get_salaire_brut(self
):
571 somme des rémuérations brutes
574 for r
in self
.get_remunerations_brutes():
575 total
+= r
.montant_euro()
578 def get_salaire_net(self
):
580 salaire brut - charges salariales
583 for r
in self
.get_charges_salariales():
584 total_charges
+= r
.montant_euro()
585 return self
.get_salaire_brut() - total_charges
587 def get_couts_auf(self
):
589 salaire net + charges patronales
592 for r
in self
.get_charges_patronales():
593 total_charges
+= r
.montant_euro()
594 return self
.get_salaire_net() + total_charges
596 def get_remunerations_tierces(self
):
600 return [r
for r
in self
.dae_remunerations
.all() if r
.type_id
in (2, )]
602 def get_total_remunerations_tierces(self
):
604 for r
in self
.get_remunerations_tierces():
605 total
+= r
.montant_euro()
609 return self
.etat
in (DOSSIER_ETAT_REGION_FINALISATION
,
610 DOSSIER_ETAT_DRH_FINALISATION
,
611 DOSSIER_ETAT_FINALISE
)
614 # Tester l'enregistrement car les models.py sont importés au complet
615 if not reversion
.is_registered(Dossier
):
616 reversion
.register(Dossier
)
618 class DossierPiece(rh
.DossierPiece_
):
619 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
620 Ex.: Lettre de motivation.
624 class DossierComparaison(rh
.DossierComparaison_
):
626 Photo d'une comparaison salariale au moment de l'embauche.
628 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+', verbose_name
='Statut', null
=True, blank
=True, )
629 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+', verbose_name
='Classement', null
=True, blank
=True, )
634 class Remuneration(rh
.Remuneration_
):
639 class Contrat(rh
.Contrat_
):
642 # modèle de liaison entre les systèmes
644 class ImportDossier(models
.Model
):
645 dae
= models
.ForeignKey('dae.Dossier', related_name
='+')
646 rh
= models
.ForeignKey('rh.Dossier', related_name
='+')
648 class ImportPoste(models
.Model
):
649 dae
= models
.ForeignKey('dae.Poste', related_name
='+')
650 rh
= models
.ForeignKey('rh.Poste', related_name
='+')