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
9 from workflow
import PosteWorkflow
, DossierWorkflow
10 from workflow
import dae_groupes
, \
11 grp_administrateurs
, \
13 grp_directeurs_bureau
, \
16 grp_haute_direction
, \
17 grp_service_utilisateurs
, \
18 grp_directeurs_service
, \
21 import datamaster_modeles
.models
as ref
22 from rh_v1
import models
as rh
23 from utils
import is_user_dans_service
, get_employe_from_user
25 STATUT_RESIDENCE_CHOICES
= (
27 ('expat', 'Expatrié'),
30 POSTE_APPEL_CHOICES
= (
31 ('interne', 'Interne'),
32 ('externe', 'Externe'),
35 POSTE_STATUT_CHOICES
= (
36 ('MAD', 'Mise à disposition'),
37 ('DET', 'Détachement'),
41 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
, base_url
=settings
.PRIVE_MEDIA_URL
)
43 def poste_piece_dispatch(instance
, filename
):
44 path
= "poste/%s/%s" % (instance
.poste_id
, filename
)
47 def dossier_piece_dispatch(instance
, filename
):
48 path
= "dossier/%s/%s" % (instance
.dossier_id
, filename
)
52 class PostePiece(models
.Model
):
53 poste
= models
.ForeignKey("Poste")
54 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
55 fichier
= models
.FileField(verbose_name
="Fichier", upload_to
=poste_piece_dispatch
, storage
=storage_prive
)
57 class PosteManager(models
.Manager
):
59 Chargement de tous les objets FK existants sur chaque QuerySet.
61 def get_query_set(self
):
73 return super(PosteManager
, self
).get_query_set() \
74 .select_related(*fkeys
).all()
76 def ma_region_ou_service(self
, user
):
78 Filtrage des postes en fonction du user connecté (region / service)
79 On s'intéresse aussi au groupe auquel appartient le user car certains groupes
83 employe
= get_employe_from_user(user
)
85 ############################################
87 ############################################
90 if is_user_dans_service(user
):
91 q
= Q(implantation
=employe
.implantation
)
94 q
= Q(implantation__region
=employe
.implantation
.region
)
95 liste
= self
.get_query_set().filter(q
)
97 ############################################
98 # TRAITEMENT POLE FINANCIER
99 ############################################
100 if grp_pole_financier
in user
.groups
.all():
101 liste
= self
.get_query_set().filter(etat
=POSTE_ETAT_FINANCE
)
103 ############################################
104 # TRAITEMENT HAUTE DIRECTION
105 ############################################
106 if grp_pole_financier
in user
.groups
.all():
107 liste
= self
.get_query_set().filter(etat
=POSTE_ETAT_HAUTE_DIRECTION
)
109 ############################################
111 ############################################
112 if grp_drh
in user
.groups
.all():
113 liste
= self
.get_query_set()
117 class Poste(PosteWorkflow
, models
.Model
):
119 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
121 verbose_name
="Mise à jour du poste")
122 nom
= models
.CharField(verbose_name
="Titre du poste", max_length
=255)
123 implantation
= models
.ForeignKey(ref
.Implantation
)
124 type_poste
= models
.ForeignKey(rh
.TypePoste
, null
=True, related_name
='+')
125 service
= models
.ForeignKey(rh
.Service
, related_name
='+',
126 verbose_name
=u
"Direction/Service/Pôle support")
127 responsable
= models
.ForeignKey(rh
.Poste
, related_name
='+',
128 verbose_name
="Poste du responsable")
130 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
132 verbose_name
="Temps de travail",
133 help_text
="% du temps complet")
134 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
137 verbose_name
="Nb. heures par semaine")
140 local
= models
.BooleanField(verbose_name
="Local", default
=True, blank
=True)
141 expatrie
= models
.BooleanField(verbose_name
="Expatrié", default
=False, blank
=True)
144 mise_a_disposition
= models
.BooleanField(verbose_name
="Mise à disposition")
145 appel
= models
.CharField(max_length
=10, default
='interne',
146 verbose_name
="Appel à candidature",
147 choices
=POSTE_APPEL_CHOICES
)
150 classement_min
= models
.ForeignKey(rh
.Classement
, related_name
='+')
151 classement_max
= models
.ForeignKey(rh
.Classement
, related_name
='+')
153 # En fait, les coefficient n'ont pas de valeur dans ces cas, les couts sont calculés
154 # et mis dans les coûts globals
155 #coefficient_min = models.FloatField(null=True) # pour classement "hors grille"
156 #coefficient_max = models.FloatField(null=True) # pour classement "hors grille"
158 valeur_point_min
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+', blank
=True, null
=True)
159 valeur_point_max
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+', blank
=True, null
=True)
160 devise_min
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
161 devise_max
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
162 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
164 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
166 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
168 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
170 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
172 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
175 # Comparatifs de rémunération
176 devise_comparaison
= models
.ForeignKey(rh
.Devise
, related_name
='+',
178 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
179 null
=True, blank
=True)
180 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
181 null
=True, blank
=True)
182 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
183 null
=True, blank
=True)
184 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
185 null
=True, blank
=True)
186 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
187 null
=True, blank
=True)
188 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
189 null
=True, blank
=True)
190 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
191 null
=True, blank
=True)
192 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
193 null
=True, blank
=True)
194 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
195 null
=True, blank
=True)
196 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
197 null
=True, blank
=True)
200 justification
= models
.TextField()
203 date_creation
= models
.DateTimeField(auto_now_add
=True)
204 date_modification
= models
.DateTimeField(auto_now
=True)
205 date_debut
= models
.DateField(verbose_name
="Date de début",
206 help_text
="format: aaaa-mm-jj")
207 date_fin
= models
.DateField(null
=True, blank
=True,
208 verbose_name
="Date de fin",
209 help_text
="format: aaaa-mm-jj")
210 actif
= models
.BooleanField(default
=True)
213 objects
= PosteManager()
217 Les vues sont montées selon une clef spéciale pour identifier la provenance du poste.
218 Cette méthode fournit un moyen de reconstruire cette clef afin de générer les URLs.
220 return "dae-%s" % self
.id
221 key
= property(_get_key
)
223 def get_dossiers(self
):
225 Liste tous les anciens dossiers liés à ce poste.
226 (Le nom de la relation sur le rh.Poste est mal choisi poste1 au lieu de dossier1)
227 Note1 : seulement le dosssier principal fait l'objet de la recherche.
228 Note2 : les dossiers sont retournés du plus récent au plus vieux. (Ce test est fait
229 en fonction du id, car les dates de création sont absentes de rh v1).
231 if self
.id_rh
is None:
233 postes
= [p
for p
in self
.id_rh
.poste1
.all()]
234 return sorted(postes
, key
=lambda poste
: poste
.id, reverse
=True)
236 def get_complement_nom(self
):
238 Inspecte les modèles rh v1 pour trouver dans le dernier dossier un complément de titre de poste.
240 dossiers
= self
.get_dossiers()
241 if len(dossiers
) > 0:
242 nom
= dossiers
[0].complement1
247 def get_employe(self
):
249 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
251 dossiers
= self
.get_dossiers()
252 if len(dossiers
) > 0:
253 return dossiers
[0].employe
257 def get_default_devise(self
):
258 """Récupère la devise par défaut en fonction de l'implantation (EUR autrement)"""
260 implantation_devise
= rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
)[0].devise
262 implantation_devise
= 5 # EUR
263 return implantation_devise
265 #####################
266 # Classement de poste
267 #####################
269 def get_couts_minimum(self
):
270 return (float)(self
.salaire_min
+ self
.indemn_min
+ self
.autre_min
)
272 def get_taux_minimum(self
):
274 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_min
)[0].taux
278 def get_couts_minimum_euros(self
):
279 return self
.get_couts_minimum() * self
.get_taux_minimum()
281 def get_couts_maximum(self
):
282 return (float)(self
.salaire_max
+ self
.indemn_max
+ self
.autre_max
)
284 def get_taux_maximum(self
):
286 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_max
)[0].taux
290 def get_couts_maximum_euros(self
):
291 return self
.get_couts_maximum() * self
.get_taux_maximum()
293 ######################
294 # Comparaison de poste
295 ######################
297 def get_taux_comparaison(self
):
299 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_comparaison
)[0].taux
303 def get_comp_universite_min_euros(self
):
304 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
306 def get_comp_fonctionpub_min_euros(self
):
307 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
309 def get_comp_locale_min_euros(self
):
310 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
312 def get_comp_ong_min_euros(self
):
313 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
315 def get_comp_autre_min_euros(self
):
316 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
318 def get_comp_universite_max_euros(self
):
319 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
321 def get_comp_fonctionpub_max_euros(self
):
322 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
324 def get_comp_locale_max_euros(self
):
325 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
327 def get_comp_ong_max_euros(self
):
328 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
330 def get_comp_autre_max_euros(self
):
331 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
334 def __unicode__(self
):
336 Cette fonction est consommatrice SQL car elle cherche les dossiers qui ont été liés à celui-ci.
338 complement_nom_poste
= self
.get_complement_nom()
339 if complement_nom_poste
is None:
340 complement_nom_poste
= ""
341 employe
= self
.get_employe()
349 complement_nom_poste
,
352 return u
'%s - %s (%s) [dae-%s %s %s]' % data
355 # Tester l'enregistrement car les models.py sont importés au complet
356 if not reversion
.is_registered(Poste
):
357 reversion
.register(Poste
)
360 POSTE_FINANCEMENT_CHOICES
= (
361 ('A', 'A - Frais de personnel'),
362 ('B', 'B - Projet(s)-Titre(s)'),
367 class PosteFinancement(models
.Model
):
368 poste
= models
.ForeignKey('Poste', related_name
='financements')
369 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
370 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
371 help_text
="ex.: 33.33 % (décimale avec point)")
372 commentaire
= models
.TextField(
373 help_text
="Spécifiez la source de financement.")
378 def __unicode__(self
):
379 return u
"%s %s %s" % (self
.get_type_display(), self
.pourcentage
, self
.commentaire
)
387 class Employe(models
.Model
):
390 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
391 verbose_name
='Employé')
392 nom
= models
.CharField(max_length
=255)
393 prenom
= models
.CharField(max_length
=255, verbose_name
='Prénom')
394 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
,
395 null
=True, blank
=True)
397 def __unicode__(self
):
398 return u
'%s %s' % (self
.prenom
, self
.nom
)
401 COMPTE_COMPTA_CHOICES
= (
407 class DossierPiece(models
.Model
):
408 dossier
= models
.ForeignKey("Dossier")
409 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
410 fichier
= models
.FileField(verbose_name
="Fichier", upload_to
=dossier_piece_dispatch
, storage
=storage_prive
)
412 class Dossier(DossierWorkflow
, models
.Model
):
415 employe
= models
.ForeignKey('Employe', related_name
='+', editable
=False)
416 poste
= models
.ForeignKey('Poste', related_name
='+', editable
=False)
417 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+')
418 organisme_bstg
= models
.ForeignKey(rh
.OrganismeBstg
,
419 null
=True, blank
=True,
420 verbose_name
="Organisme",
421 help_text
="Si détaché (DET) ou mis à disposition (MAD), \
422 préciser l'organisme.",
424 organisme_bstg_autre
= models
.CharField(max_length
=255,
425 verbose_name
="Autre organisme",
426 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
430 # Données antérieures de l'employé
431 statut_anterieur
= models
.ForeignKey(
432 rh
.Statut
, related_name
='+', null
=True, blank
=True,
433 verbose_name
='Statut antérieur')
434 classement_anterieur
= models
.ForeignKey(
435 rh
.Classement
, related_name
='+', null
=True, blank
=True,
436 verbose_name
='Classement précédent')
437 salaire_anterieur
= models
.DecimalField(
438 max_digits
=12, decimal_places
=2, null
=True, default
=None,
439 blank
=True, verbose_name
='Salaire précédent')
441 # Données du titulaire précédent
442 employe_anterieur
= models
.ForeignKey(
443 rh
.Employe
, related_name
='+', null
=True, blank
=True,
444 verbose_name
='Employé précédent')
445 statut_titulaire_anterieur
= models
.ForeignKey(
446 rh
.Statut
, related_name
='+', null
=True, blank
=True,
447 verbose_name
='Statut titulaire précédent')
448 classement_titulaire_anterieur
= models
.ForeignKey(
449 rh
.Classement
, related_name
='+', null
=True, blank
=True,
450 verbose_name
='Classement titulaire précédent')
451 salaire_titulaire_anterieur
= models
.DecimalField(
452 max_digits
=12, decimal_places
=2, default
=None, null
=True,
453 blank
=True, verbose_name
='Salaire titulaire précédent')
456 remplacement
= models
.BooleanField()
457 statut_residence
= models
.CharField(max_length
=10, default
='local',
458 verbose_name
="Statut",
459 choices
=STATUT_RESIDENCE_CHOICES
)
462 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+',
463 verbose_name
='Classement proposé')
464 salaire
= models
.DecimalField(max_digits
=12, decimal_places
=2,
465 verbose_name
='Salaire de base',
466 null
=True, default
=None)
467 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
468 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
469 verbose_name
="Régime de travail",
470 help_text
="% du temps complet")
471 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
472 decimal_places
=2, verbose_name
="Nb. heures par semaine")
475 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
476 contrat_date_debut
= models
.DateField(help_text
="format: aaaa-mm-jj")
477 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
478 help_text
="format: aaaa-mm-jj")
481 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
482 verbose_name
=u
'Compte comptabilité',
483 choices
=COMPTE_COMPTA_CHOICES
)
484 compte_courriel
= models
.BooleanField()
487 date_creation
= models
.DateTimeField(auto_now_add
=True)
489 def __unicode__(self
):
490 return u
'%s - %s' % (self
.poste
.nom
, self
.employe
)
492 def get_salaire_euros(self
):
494 tx
= rh
.TauxChange
.objects
.filter(implantation
=self
.poste
.implantation
, devise
=self
.devise
)[0].taux
497 return (float)(tx
) * (float)(self
.salaire
)
499 def get_couts_auf(self
):
501 On retire les MAD BSTG
503 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
not in (2, )]
505 def get_total_couts_auf(self
):
507 for r
in self
.get_couts_auf():
508 total
+= r
.montant_euro()
511 def get_aides_auf(self
):
513 On récupère les MAD BSTG
515 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
in (2, )]
517 def get_total_aides_auf(self
):
519 for r
in self
.get_aides_auf():
520 total
+= r
.montant_euro()
523 def get_total_remun(self
):
524 return self
.get_total_couts_auf() + self
.get_total_aides_auf()
526 # Tester l'enregistrement car les models.py sont importés au complet
527 if not reversion
.is_registered(Dossier
):
528 reversion
.register(Dossier
)
530 class Remuneration(models
.Model
):
532 dossier
= models
.ForeignKey('Dossier', db_column
='dossier')
533 type = models
.ForeignKey(rh
.TypeRemuneration
, db_column
='type',
536 # type_revalorisation = models.ForeignKey('TypeRevalorisation',
537 # db_column='type_revalorisation')
538 montant
= models
.DecimalField(max_digits
=12, decimal_places
=2,
540 devise
= models
.ForeignKey(rh
.Devise
, to_field
='code',
541 db_column
='devise', related_name
='+')
542 precision
= models
.CharField(max_length
=255, null
=True, blank
=True)
543 # date_effective = models.DateField(null=True, blank=True)
544 # pourcentage = models.IntegerField(null=True, blank=True)
547 date_creation
= models
.DateField(auto_now_add
=True)
548 user_creation
= models
.IntegerField(null
=True, blank
=True)
549 # desactivation = models.BooleanField(default=False, blank=True)
550 # date_desactivation = models.DateField(null=True, blank=True)
551 # user_desactivation = models.IntegerField(null=True, blank=True)
552 # annulation = models.BooleanField(default=False, blank=True)
553 # date_annulation = models.DateField(null=True, blank=True)
554 # user_annulation = models.IntegerField(null=True, blank=True)
556 def montant_mois(self
):
557 return round(self
.montant
/ 12, 2)
559 def taux_devise(self
):
560 return self
.devise
.tauxchange_set
.order_by('-annee').all()[0].taux
562 def montant_euro(self
):
563 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
565 def montant_euro_mois(self
):
566 return round(self
.montant_euro() / 12, 2)
569 TYPE_JUSTIFICATIONS
= (
570 ('N', 'Nouvel employé'),
571 ('R', 'Renouvellement employé'),
574 class JustificationQuestion(models
.Model
):
575 question
= models
.CharField(max_length
=255)
576 type = models
.CharField(max_length
=255, choices
=TYPE_JUSTIFICATIONS
)
578 def __unicode__(self
,):
581 class JustificationNouvelEmploye(models
.Model
):
582 dossier
= models
.ForeignKey("Dossier")
583 question
= models
.ForeignKey("JustificationQuestion")
584 reponse
= models
.TextField()
586 class JustificationAutreEmploye(models
.Model
):
587 dossier
= models
.ForeignKey("Dossier")
588 question
= models
.ForeignKey("JustificationQuestion")
589 reponse
= models
.TextField()