1 # -=- encoding: utf-8 -=-
4 from django
.conf
import settings
5 from django
.core
.files
.storage
import FileSystemStorage
6 from django
.db
import models
7 from django
.db
.models
import Q
8 from django
.contrib
.auth
.models
import Group
10 from workflow
import PosteWorkflow
, DossierWorkflow
11 import datamaster_modeles
.models
as ref
12 from rh_v1
import models
as rh
13 from utils
import is_user_dans_service
, get_employe_from_user
15 # Groupes impliqués dans le Worflow
16 grp_administrateurs
, created
= Group
.objects
.get_or_create(name
='Administrateurs')
17 grp_gestionnaires
, created
= Group
.objects
.get_or_create(name
='Gestionnaires')
18 grp_directeurs_bureau
, created
= Group
.objects
.get_or_create(name
='Directeurs de bureau')
19 grp_drh
, created
= Group
.objects
.get_or_create(name
='DRH')
20 grp_pole_financier
, created
= Group
.objects
.get_or_create(name
='Pôle financier')
21 grp_haute_direction
, created
= Group
.objects
.get_or_create(name
='Haute direction')
22 grp_service_utilisateurs
, created
= Group
.objects
.get_or_create(name
='Service utilisateurs')
23 grp_directeurs_service
, created
= Group
.objects
.get_or_create(name
='Directeurs de service / pôle')
24 grp_correspondants_rh
, created
= Group
.objects
.get_or_create(name
='Correspondants RH')
26 dae_groupes
= (grp_administrateurs
, grp_gestionnaires
, grp_directeurs_bureau
, grp_drh
,
27 grp_pole_financier
, grp_haute_direction
, grp_service_utilisateurs
,
28 grp_directeurs_service
, grp_correspondants_rh
, )
30 STATUT_RESIDENCE_CHOICES
= (
32 ('expat', 'Expatrié'),
35 POSTE_APPEL_CHOICES
= (
36 ('interne', 'Interne'),
37 ('externe', 'Externe'),
40 POSTE_STATUT_CHOICES
= (
41 ('MAD', 'Mise à disposition'),
42 ('DET', 'Détachement'),
46 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
, base_url
=settings
.PRIVE_MEDIA_URL
)
48 def poste_piece_dispatch(instance
, filename
):
49 path
= "poste/%s/%s" % (instance
.poste_id
, filename
)
52 def dossier_piece_dispatch(instance
, filename
):
53 path
= "dossier/%s/%s" % (instance
.dossier_id
, filename
)
57 class PostePiece(models
.Model
):
58 poste
= models
.ForeignKey("Poste")
59 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
60 fichier
= models
.FileField(verbose_name
="Fichier", upload_to
=poste_piece_dispatch
, storage
=storage_prive
)
62 class PosteManager(models
.Manager
):
64 Chargement de tous les objets FK existants sur chaque QuerySet.
66 def get_query_set(self
):
78 return super(PosteManager
, self
).get_query_set() \
79 .select_related(*fkeys
).all()
81 def ma_region_ou_service(self
, user
):
83 Filtrage des postes en fonction du user connecté (region / service)
86 employe
= get_employe_from_user(user
)
87 if is_user_dans_service(user
):
88 q
= Q(implantation
=employe
.implantation
)
91 q
= Q(implantation__region
=employe
.implantation
.region
)
92 return self
.get_query_set().filter(q
)
94 class Poste(PosteWorkflow
, models
.Model
):
96 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
98 verbose_name
="Mise à jour du poste")
99 nom
= models
.CharField(verbose_name
="Titre du poste", max_length
=255)
100 implantation
= models
.ForeignKey(ref
.Implantation
)
101 type_poste
= models
.ForeignKey(rh
.TypePoste
, null
=True, related_name
='+')
102 service
= models
.ForeignKey(rh
.Service
, related_name
='+',
103 verbose_name
=u
"Direction/Service/Pôle support")
104 responsable
= models
.ForeignKey(rh
.Poste
, related_name
='+',
105 verbose_name
="Poste du responsable")
107 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
109 verbose_name
="Temps de travail",
110 help_text
="% du temps complet")
111 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
114 verbose_name
="Nb. heures par semaine")
117 local
= models
.BooleanField(verbose_name
="Local", default
=True, blank
=True)
118 expatrie
= models
.BooleanField(verbose_name
="Expatrié", default
=False, blank
=True)
121 mise_a_disposition
= models
.BooleanField(verbose_name
="Mise à disposition")
122 appel
= models
.CharField(max_length
=10, default
='interne',
123 verbose_name
="Appel à candidature",
124 choices
=POSTE_APPEL_CHOICES
)
127 classement_min
= models
.ForeignKey(rh
.Classement
, related_name
='+')
128 classement_max
= models
.ForeignKey(rh
.Classement
, related_name
='+')
130 # En fait, les coefficient n'ont pas de valeur dans ces cas, les couts sont calculés
131 # et mis dans les coûts globals
132 #coefficient_min = models.FloatField(null=True) # pour classement "hors grille"
133 #coefficient_max = models.FloatField(null=True) # pour classement "hors grille"
135 valeur_point_min
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+', blank
=True, null
=True)
136 valeur_point_max
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+', blank
=True, null
=True)
137 devise_min
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
138 devise_max
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
139 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
141 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
143 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
145 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
149 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
152 # Comparatifs de rémunération
153 devise_comparaison
= models
.ForeignKey(rh
.Devise
, related_name
='+',
155 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
156 null
=True, blank
=True)
157 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
158 null
=True, blank
=True)
159 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
160 null
=True, blank
=True)
161 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
162 null
=True, blank
=True)
163 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
164 null
=True, blank
=True)
165 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
166 null
=True, blank
=True)
167 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
168 null
=True, blank
=True)
169 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
170 null
=True, blank
=True)
171 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
172 null
=True, blank
=True)
173 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
174 null
=True, blank
=True)
177 justification
= models
.TextField()
180 date_creation
= models
.DateTimeField(auto_now_add
=True)
181 date_modification
= models
.DateTimeField(auto_now
=True)
182 date_debut
= models
.DateField(verbose_name
="Date de début",
183 help_text
="format: aaaa-mm-jj")
184 date_fin
= models
.DateField(null
=True, blank
=True,
185 verbose_name
="Date de fin",
186 help_text
="format: aaaa-mm-jj")
187 actif
= models
.BooleanField(default
=True)
190 objects
= PosteManager()
194 Les vues sont montées selon une clef spéciale pour identifier la provenance du poste.
195 Cette méthode fournit un moyen de reconstruire cette clef afin de générer les URLs.
197 return "dae-%s" % self
.id
198 key
= property(_get_key
)
200 def get_dossiers(self
):
202 Liste tous les anciens dossiers liés à ce poste.
203 (Le nom de la relation sur le rh.Poste est mal choisi poste1 au lieu de dossier1)
204 Note1 : seulement le dosssier principal fait l'objet de la recherche.
205 Note2 : les dossiers sont retournés du plus récent au plus vieux. (Ce test est fait
206 en fonction du id, car les dates de création sont absentes de rh v1).
208 if self
.id_rh
is None:
210 postes
= [p
for p
in self
.id_rh
.poste1
.all()]
211 return sorted(postes
, key
=lambda poste
: poste
.id, reverse
=True)
213 def get_complement_nom(self
):
215 Inspecte les modèles rh v1 pour trouver dans le dernier dossier un complément de titre de poste.
217 dossiers
= self
.get_dossiers()
218 if len(dossiers
) > 0:
219 nom
= dossiers
[0].complement1
224 def get_employe(self
):
226 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
228 dossiers
= self
.get_dossiers()
229 if len(dossiers
) > 0:
230 return dossiers
[0].employe
234 def get_default_devise(self
):
235 """Récupère la devise par défaut en fonction de l'implantation (EUR autrement)"""
237 implantation_devise
= rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
)[0].devise
239 implantation_devise
= 5 # EUR
240 return implantation_devise
242 #####################
243 # Classement de poste
244 #####################
246 def get_couts_minimum(self
):
247 return (float)(self
.salaire_min
+ self
.indemn_min
+ self
.autre_min
)
249 def get_taux_minimum(self
):
251 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_min
)[0].taux
255 def get_couts_minimum_euros(self
):
256 return self
.get_couts_minimum() * self
.get_taux_minimum()
258 def get_couts_maximum(self
):
259 return (float)(self
.salaire_max
+ self
.indemn_max
+ self
.autre_max
)
261 def get_taux_maximum(self
):
263 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_max
)[0].taux
267 def get_couts_maximum_euros(self
):
268 return self
.get_couts_maximum() * self
.get_taux_maximum()
270 ######################
271 # Comparaison de poste
272 ######################
274 def get_taux_comparaison(self
):
276 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_comparaison
)[0].taux
280 def get_comp_universite_min_euros(self
):
281 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
283 def get_comp_fonctionpub_min_euros(self
):
284 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
286 def get_comp_locale_min_euros(self
):
287 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
289 def get_comp_ong_min_euros(self
):
290 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
292 def get_comp_autre_min_euros(self
):
293 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
295 def get_comp_universite_max_euros(self
):
296 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
298 def get_comp_fonctionpub_max_euros(self
):
299 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
301 def get_comp_locale_max_euros(self
):
302 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
304 def get_comp_ong_max_euros(self
):
305 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
307 def get_comp_autre_max_euros(self
):
308 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
311 def __unicode__(self
):
313 Cette fonction est consommatrice SQL car elle cherche les dossiers qui ont été liés à celui-ci.
315 complement_nom_poste
= self
.get_complement_nom()
316 if complement_nom_poste
is None:
317 complement_nom_poste
= ""
318 employe
= self
.get_employe()
326 complement_nom_poste
,
329 return u
'%s - %s (%s) [dae-%s %s %s]' % data
332 # Tester l'enregistrement car les models.py sont importés au complet
333 if not reversion
.is_registered(Poste
):
334 reversion
.register(Poste
)
337 POSTE_FINANCEMENT_CHOICES
= (
338 ('A', 'A - Frais de personnel'),
339 ('B', 'B - Projet(s)-Titre(s)'),
344 class PosteFinancement(models
.Model
):
345 poste
= models
.ForeignKey('Poste', related_name
='financements')
346 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
347 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
348 help_text
="ex.: 33.33 % (décimale avec point)")
349 commentaire
= models
.TextField(
350 help_text
="Spécifiez la source de financement.")
355 def __unicode__(self
):
356 return u
"%s %s %s" % (self
.get_type_display(), self
.pourcentage
, self
.commentaire
)
364 class Employe(models
.Model
):
367 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
368 verbose_name
='Employé')
369 nom
= models
.CharField(max_length
=255)
370 prenom
= models
.CharField(max_length
=255, verbose_name
='Prénom')
371 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
,
372 null
=True, blank
=True)
374 def __unicode__(self
):
375 return u
'%s %s' % (self
.prenom
, self
.nom
)
378 COMPTE_COMPTA_CHOICES
= (
384 class DossierPiece(models
.Model
):
385 dossier
= models
.ForeignKey("Dossier")
386 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
387 fichier
= models
.FileField(verbose_name
="Fichier", upload_to
=dossier_piece_dispatch
, storage
=storage_prive
)
389 class Dossier(DossierWorkflow
, models
.Model
):
392 employe
= models
.ForeignKey('Employe', related_name
='+', editable
=False)
393 poste
= models
.ForeignKey('Poste', related_name
='+', editable
=False)
394 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+')
395 organisme_bstg
= models
.ForeignKey(rh
.OrganismeBstg
,
396 null
=True, blank
=True,
397 verbose_name
="Organisme",
398 help_text
="Si détaché (DET) ou mis à disposition (MAD), \
399 préciser l'organisme.",
401 organisme_bstg_autre
= models
.CharField(max_length
=255,
402 verbose_name
="Autre organisme",
403 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
407 # Données antérieures de l'employé
408 statut_anterieur
= models
.ForeignKey(
409 rh
.Statut
, related_name
='+', null
=True, blank
=True,
410 verbose_name
='Statut antérieur')
411 classement_anterieur
= models
.ForeignKey(
412 rh
.Classement
, related_name
='+', null
=True, blank
=True,
413 verbose_name
='Classement précédent')
414 salaire_anterieur
= models
.DecimalField(
415 max_digits
=12, decimal_places
=2, null
=True, default
=None,
416 blank
=True, verbose_name
='Salaire précédent')
418 # Données du titulaire précédent
419 employe_anterieur
= models
.ForeignKey(
420 rh
.Employe
, related_name
='+', null
=True, blank
=True,
421 verbose_name
='Employé précédent')
422 statut_titulaire_anterieur
= models
.ForeignKey(
423 rh
.Statut
, related_name
='+', null
=True, blank
=True,
424 verbose_name
='Statut titulaire précédent')
425 classement_titulaire_anterieur
= models
.ForeignKey(
426 rh
.Classement
, related_name
='+', null
=True, blank
=True,
427 verbose_name
='Classement titulaire précédent')
428 salaire_titulaire_anterieur
= models
.DecimalField(
429 max_digits
=12, decimal_places
=2, default
=None, null
=True,
430 blank
=True, verbose_name
='Salaire titulaire précédent')
433 remplacement
= models
.BooleanField()
434 statut_residence
= models
.CharField(max_length
=10, default
='local',
435 verbose_name
="Statut",
436 choices
=STATUT_RESIDENCE_CHOICES
)
439 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+',
440 verbose_name
='Classement proposé')
441 salaire
= models
.DecimalField(max_digits
=12, decimal_places
=2,
442 verbose_name
='Salaire de base',
443 null
=True, default
=None)
444 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
445 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
446 verbose_name
="Régime de travail",
447 help_text
="% du temps complet")
448 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
449 decimal_places
=2, verbose_name
="Nb. heures par semaine")
452 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
453 contrat_date_debut
= models
.DateField(help_text
="format: aaaa-mm-jj")
454 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
455 help_text
="format: aaaa-mm-jj")
458 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
459 verbose_name
=u
'Compte comptabilité',
460 choices
=COMPTE_COMPTA_CHOICES
)
461 compte_courriel
= models
.BooleanField()
464 date_creation
= models
.DateTimeField(auto_now_add
=True)
466 def __unicode__(self
):
467 return u
'%s - %s' % (self
.poste
.nom
, self
.employe
)
469 def get_salaire_euros(self
):
471 tx
= rh
.TauxChange
.objects
.filter(implantation
=self
.poste
.implantation
, devise
=self
.devise
)[0].taux
474 return (float)(tx
) * (float)(self
.salaire
)
476 def get_couts_auf(self
):
478 On retire les MAD BSTG
480 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
not in (2, )]
482 def get_total_couts_auf(self
):
484 for r
in self
.get_couts_auf():
485 total
+= r
.montant_euro()
488 def get_aides_auf(self
):
490 On récupère les MAD BSTG
492 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
in (2, )]
494 def get_total_aides_auf(self
):
496 for r
in self
.get_aides_auf():
497 total
+= r
.montant_euro()
500 def get_total_remun(self
):
501 return self
.get_total_couts_auf() + self
.get_total_aides_auf()
503 # Tester l'enregistrement car les models.py sont importés au complet
504 if not reversion
.is_registered(Dossier
):
505 reversion
.register(Dossier
)
507 class Remuneration(models
.Model
):
509 dossier
= models
.ForeignKey('Dossier', db_column
='dossier')
510 type = models
.ForeignKey(rh
.TypeRemuneration
, db_column
='type',
513 # type_revalorisation = models.ForeignKey('TypeRevalorisation',
514 # db_column='type_revalorisation')
515 montant
= models
.DecimalField(max_digits
=12, decimal_places
=2,
517 devise
= models
.ForeignKey(rh
.Devise
, to_field
='code',
518 db_column
='devise', related_name
='+')
519 precision
= models
.CharField(max_length
=255, null
=True, blank
=True)
520 # date_effective = models.DateField(null=True, blank=True)
521 # pourcentage = models.IntegerField(null=True, blank=True)
524 date_creation
= models
.DateField(auto_now_add
=True)
525 user_creation
= models
.IntegerField(null
=True, blank
=True)
526 # desactivation = models.BooleanField(default=False, blank=True)
527 # date_desactivation = models.DateField(null=True, blank=True)
528 # user_desactivation = models.IntegerField(null=True, blank=True)
529 # annulation = models.BooleanField(default=False, blank=True)
530 # date_annulation = models.DateField(null=True, blank=True)
531 # user_annulation = models.IntegerField(null=True, blank=True)
533 def montant_mois(self
):
534 return round(self
.montant
/ 12, 2)
536 def taux_devise(self
):
537 return self
.devise
.tauxchange_set
.order_by('-annee').all()[0].taux
539 def montant_euro(self
):
540 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
542 def montant_euro_mois(self
):
543 return round(self
.montant_euro() / 12, 2)
546 TYPE_JUSTIFICATIONS
= (
547 ('N', 'Nouvel employé'),
548 ('R', 'Renouvellement employé'),
551 class JustificationQuestion(models
.Model
):
552 question
= models
.CharField(max_length
=255)
553 type = models
.CharField(max_length
=255, choices
=TYPE_JUSTIFICATIONS
)
555 def __unicode__(self
,):
558 class JustificationNouvelEmploye(models
.Model
):
559 dossier
= models
.ForeignKey("Dossier")
560 question
= models
.ForeignKey("JustificationQuestion")
561 reponse
= models
.TextField()
563 class JustificationAutreEmploye(models
.Model
):
564 dossier
= models
.ForeignKey("Dossier")
565 question
= models
.ForeignKey("JustificationQuestion")
566 reponse
= models
.TextField()