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
, \
20 from workflow
import POSTE_ETAT_HAUTE_DIRECTION
, POSTE_ETAT_POLE_FINANCIER
22 import datamaster_modeles
.models
as ref
23 from rh_v1
import models
as rh
24 from utils
import is_user_dans_service
, get_employe_from_user
26 STATUT_RESIDENCE_CHOICES
= (
28 ('expat', 'Expatrié'),
31 POSTE_APPEL_CHOICES
= (
32 ('interne', 'Interne'),
33 ('externe', 'Externe'),
36 POSTE_STATUT_CHOICES
= (
37 ('MAD', 'Mise à disposition'),
38 ('DET', 'Détachement'),
42 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
, base_url
=settings
.PRIVE_MEDIA_URL
)
44 def poste_piece_dispatch(instance
, filename
):
45 path
= "poste/%s/%s" % (instance
.poste_id
, filename
)
48 def dossier_piece_dispatch(instance
, filename
):
49 path
= "dossier/%s/%s" % (instance
.dossier_id
, filename
)
53 class PostePiece(models
.Model
):
54 poste
= models
.ForeignKey("Poste")
55 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
56 fichier
= models
.FileField(verbose_name
="Fichier", upload_to
=poste_piece_dispatch
, storage
=storage_prive
)
59 class SecurityManager(models
.Manager
):
61 prefixe_implantation
= None
63 def ma_region_ou_service(self
, user
):
65 Filtrage des postes en fonction du user connecté (region / service)
66 On s'intéresse aussi au groupe auquel appartient le user car certains groupes
70 employe
= get_employe_from_user(user
)
72 ############################################
74 ############################################
77 if is_user_dans_service(user
):
78 q
= Q(**{ '%s' % self
.prefixe_implantation
: employe
.implantation
})
81 q
= Q(**{ '%s__region' % self
.prefixe_implantation
: employe
.implantation
.region
})
82 liste
= self
.get_query_set().filter(q
)
84 ############################################
85 # TRAITEMENT POLE FINANCIER
86 ############################################
87 if grp_pole_financier
in user
.groups
.all():
88 liste
= self
.get_query_set().filter(etat
=POSTE_ETAT_FINANCE
)
90 ############################################
91 # TRAITEMENT HAUTE DIRECTION
92 ############################################
93 if grp_haute_direction
in user
.groups
.all():
94 liste
= self
.get_query_set().filter(etat
=POSTE_ETAT_HAUTE_DIRECTION
)
96 ############################################
98 ############################################
99 if grp_drh
in user
.groups
.all():
100 liste
= self
.get_query_set()
105 class PosteManager(SecurityManager
):
107 Chargement de tous les objets FK existants sur chaque QuerySet.
109 prefixe_implantation
= "implantation"
111 def get_query_set(self
):
123 return super(PosteManager
, self
).get_query_set() \
124 .select_related(*fkeys
).all()
127 class Poste(PosteWorkflow
, models
.Model
):
129 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
131 verbose_name
="Mise à jour du poste")
132 nom
= models
.CharField(verbose_name
="Titre du poste", max_length
=255)
133 implantation
= models
.ForeignKey(ref
.Implantation
)
134 type_poste
= models
.ForeignKey(rh
.TypePoste
, null
=True, related_name
='+')
135 service
= models
.ForeignKey(rh
.Service
, related_name
='+',
136 verbose_name
=u
"Direction/Service/Pôle support")
137 responsable
= models
.ForeignKey(rh
.Poste
, related_name
='+',
138 verbose_name
="Poste du responsable")
140 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
142 verbose_name
="Temps de travail",
143 help_text
="% du temps complet")
144 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
147 verbose_name
="Nb. heures par semaine")
150 local
= models
.BooleanField(verbose_name
="Local", default
=True, blank
=True)
151 expatrie
= models
.BooleanField(verbose_name
="Expatrié", default
=False, blank
=True)
154 mise_a_disposition
= models
.BooleanField(verbose_name
="Mise à disposition")
155 appel
= models
.CharField(max_length
=10, default
='interne',
156 verbose_name
="Appel à candidature",
157 choices
=POSTE_APPEL_CHOICES
)
160 classement_min
= models
.ForeignKey(rh
.Classement
, related_name
='+')
161 classement_max
= models
.ForeignKey(rh
.Classement
, related_name
='+')
163 # En fait, les coefficient n'ont pas de valeur dans ces cas, les couts sont calculés
164 # et mis dans les coûts globals
165 #coefficient_min = models.FloatField(null=True) # pour classement "hors grille"
166 #coefficient_max = models.FloatField(null=True) # pour classement "hors grille"
168 valeur_point_min
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+', blank
=True, null
=True)
169 valeur_point_max
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+', blank
=True, null
=True)
170 devise_min
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
171 devise_max
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
172 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
174 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
176 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
178 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
180 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
182 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
185 # Comparatifs de rémunération
186 devise_comparaison
= models
.ForeignKey(rh
.Devise
, related_name
='+',
188 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
189 null
=True, blank
=True)
190 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
191 null
=True, blank
=True)
192 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
193 null
=True, blank
=True)
194 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
195 null
=True, blank
=True)
196 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
197 null
=True, blank
=True)
198 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
199 null
=True, blank
=True)
200 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
201 null
=True, blank
=True)
202 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
203 null
=True, blank
=True)
204 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
205 null
=True, blank
=True)
206 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
207 null
=True, blank
=True)
210 justification
= models
.TextField()
213 date_creation
= models
.DateTimeField(auto_now_add
=True)
214 date_modification
= models
.DateTimeField(auto_now
=True)
215 date_debut
= models
.DateField(verbose_name
="Date de début",
216 help_text
="format: aaaa-mm-jj")
217 date_fin
= models
.DateField(null
=True, blank
=True,
218 verbose_name
="Date de fin",
219 help_text
="format: aaaa-mm-jj")
220 actif
= models
.BooleanField(default
=True)
223 objects
= PosteManager()
227 Les vues sont montées selon une clef spéciale pour identifier la provenance du poste.
228 Cette méthode fournit un moyen de reconstruire cette clef afin de générer les URLs.
230 return "dae-%s" % self
.id
231 key
= property(_get_key
)
233 def get_dossiers(self
):
235 Liste tous les anciens dossiers liés à ce poste.
236 (Le nom de la relation sur le rh.Poste est mal choisi poste1 au lieu de dossier1)
237 Note1 : seulement le dosssier principal fait l'objet de la recherche.
238 Note2 : les dossiers sont retournés du plus récent au plus vieux. (Ce test est fait
239 en fonction du id, car les dates de création sont absentes de rh v1).
241 if self
.id_rh
is None:
243 postes
= [p
for p
in self
.id_rh
.poste1
.all()]
244 return sorted(postes
, key
=lambda poste
: poste
.id, reverse
=True)
246 def get_complement_nom(self
):
248 Inspecte les modèles rh v1 pour trouver dans le dernier dossier un complément de titre de poste.
250 dossiers
= self
.get_dossiers()
251 if len(dossiers
) > 0:
252 nom
= dossiers
[0].complement1
257 def get_employe(self
):
259 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
261 dossiers
= self
.get_dossiers()
262 if len(dossiers
) > 0:
263 return dossiers
[0].employe
267 def get_default_devise(self
):
268 """Récupère la devise par défaut en fonction de l'implantation (EUR autrement)"""
270 implantation_devise
= rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
)[0].devise
272 implantation_devise
= 5 # EUR
273 return implantation_devise
275 #####################
276 # Classement de poste
277 #####################
279 def get_couts_minimum(self
):
280 return (float)(self
.salaire_min
+ self
.indemn_min
+ self
.autre_min
)
282 def get_taux_minimum(self
):
284 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_min
)[0].taux
288 def get_couts_minimum_euros(self
):
289 return self
.get_couts_minimum() * self
.get_taux_minimum()
291 def get_couts_maximum(self
):
292 return (float)(self
.salaire_max
+ self
.indemn_max
+ self
.autre_max
)
294 def get_taux_maximum(self
):
296 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_max
)[0].taux
300 def get_couts_maximum_euros(self
):
301 return self
.get_couts_maximum() * self
.get_taux_maximum()
303 ######################
304 # Comparaison de poste
305 ######################
307 def get_taux_comparaison(self
):
309 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_comparaison
)[0].taux
313 def get_comp_universite_min_euros(self
):
314 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
316 def get_comp_fonctionpub_min_euros(self
):
317 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
319 def get_comp_locale_min_euros(self
):
320 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
322 def get_comp_ong_min_euros(self
):
323 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
325 def get_comp_autre_min_euros(self
):
326 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
328 def get_comp_universite_max_euros(self
):
329 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
331 def get_comp_fonctionpub_max_euros(self
):
332 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
334 def get_comp_locale_max_euros(self
):
335 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
337 def get_comp_ong_max_euros(self
):
338 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
340 def get_comp_autre_max_euros(self
):
341 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
344 def __unicode__(self
):
346 Cette fonction est consommatrice SQL car elle cherche les dossiers qui ont été liés à celui-ci.
348 complement_nom_poste
= self
.get_complement_nom()
349 if complement_nom_poste
is None:
350 complement_nom_poste
= ""
351 employe
= self
.get_employe()
359 complement_nom_poste
,
362 return u
'%s - %s (%s) [dae-%s %s %s]' % data
365 # Tester l'enregistrement car les models.py sont importés au complet
366 if not reversion
.is_registered(Poste
):
367 reversion
.register(Poste
)
370 POSTE_FINANCEMENT_CHOICES
= (
371 ('A', 'A - Frais de personnel'),
372 ('B', 'B - Projet(s)-Titre(s)'),
377 class PosteFinancement(models
.Model
):
378 poste
= models
.ForeignKey('Poste', related_name
='financements')
379 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
380 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
381 help_text
="ex.: 33.33 % (décimale avec point)")
382 commentaire
= models
.TextField(
383 help_text
="Spécifiez la source de financement.")
388 def __unicode__(self
):
389 return u
"%s %s %s" % (self
.get_type_display(), self
.pourcentage
, self
.commentaire
)
397 class Employe(models
.Model
):
400 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
401 verbose_name
='Employé')
402 nom
= models
.CharField(max_length
=255)
403 prenom
= models
.CharField(max_length
=255, verbose_name
='Prénom')
404 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
,
405 null
=True, blank
=True)
407 def __unicode__(self
):
408 return u
'%s %s' % (self
.prenom
, self
.nom
)
411 COMPTE_COMPTA_CHOICES
= (
417 class DossierPiece(models
.Model
):
418 dossier
= models
.ForeignKey("Dossier")
419 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
420 fichier
= models
.FileField(verbose_name
="Fichier", upload_to
=dossier_piece_dispatch
, storage
=storage_prive
)
423 class DossierManager(SecurityManager
):
424 prefixe_implantation
= "poste__implantation"
426 class Dossier(DossierWorkflow
, models
.Model
):
429 employe
= models
.ForeignKey('Employe', related_name
='+', editable
=False)
430 poste
= models
.ForeignKey('Poste', related_name
='+', editable
=False)
431 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+')
432 organisme_bstg
= models
.ForeignKey(rh
.OrganismeBstg
,
433 null
=True, blank
=True,
434 verbose_name
="Organisme",
435 help_text
="Si détaché (DET) ou mis à disposition (MAD), \
436 préciser l'organisme.",
438 organisme_bstg_autre
= models
.CharField(max_length
=255,
439 verbose_name
="Autre organisme",
440 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
444 # Données antérieures de l'employé
445 statut_anterieur
= models
.ForeignKey(
446 rh
.Statut
, related_name
='+', null
=True, blank
=True,
447 verbose_name
='Statut antérieur')
448 classement_anterieur
= models
.ForeignKey(
449 rh
.Classement
, related_name
='+', null
=True, blank
=True,
450 verbose_name
='Classement précédent')
451 salaire_anterieur
= models
.DecimalField(
452 max_digits
=12, decimal_places
=2, null
=True, default
=None,
453 blank
=True, verbose_name
='Salaire précédent')
455 # Données du titulaire précédent
456 employe_anterieur
= models
.ForeignKey(
457 rh
.Employe
, related_name
='+', null
=True, blank
=True,
458 verbose_name
='Employé précédent')
459 statut_titulaire_anterieur
= models
.ForeignKey(
460 rh
.Statut
, related_name
='+', null
=True, blank
=True,
461 verbose_name
='Statut titulaire précédent')
462 classement_titulaire_anterieur
= models
.ForeignKey(
463 rh
.Classement
, related_name
='+', null
=True, blank
=True,
464 verbose_name
='Classement titulaire précédent')
465 salaire_titulaire_anterieur
= models
.DecimalField(
466 max_digits
=12, decimal_places
=2, default
=None, null
=True,
467 blank
=True, verbose_name
='Salaire titulaire précédent')
470 remplacement
= models
.BooleanField()
471 statut_residence
= models
.CharField(max_length
=10, default
='local',
472 verbose_name
="Statut",
473 choices
=STATUT_RESIDENCE_CHOICES
)
476 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+',
477 verbose_name
='Classement proposé')
478 salaire
= models
.DecimalField(max_digits
=12, decimal_places
=2,
479 verbose_name
='Salaire de base',
480 null
=True, default
=None)
481 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
482 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
483 verbose_name
="Régime de travail",
484 help_text
="% du temps complet")
485 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
486 decimal_places
=2, verbose_name
="Nb. heures par semaine")
489 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
490 contrat_date_debut
= models
.DateField(help_text
="format: aaaa-mm-jj")
491 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
492 help_text
="format: aaaa-mm-jj")
495 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
496 verbose_name
=u
'Compte comptabilité',
497 choices
=COMPTE_COMPTA_CHOICES
)
498 compte_courriel
= models
.BooleanField()
501 date_creation
= models
.DateTimeField(auto_now_add
=True)
504 objects
= DossierManager()
506 def __unicode__(self
):
507 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
509 def get_salaire_euros(self
):
511 tx
= rh
.TauxChange
.objects
.filter(implantation
=self
.poste
.implantation
, devise
=self
.devise
)[0].taux
514 return (float)(tx
) * (float)(self
.salaire
)
516 def get_couts_auf(self
):
518 On retire les MAD BSTG
520 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
not in (2, )]
522 def get_total_couts_auf(self
):
524 for r
in self
.get_couts_auf():
525 total
+= r
.montant_euro()
528 def get_aides_auf(self
):
530 On récupère les MAD BSTG
532 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
in (2, )]
534 def get_total_aides_auf(self
):
536 for r
in self
.get_aides_auf():
537 total
+= r
.montant_euro()
540 def get_total_remun(self
):
541 return self
.get_total_couts_auf() + self
.get_total_aides_auf()
543 # Tester l'enregistrement car les models.py sont importés au complet
544 if not reversion
.is_registered(Dossier
):
545 reversion
.register(Dossier
)
547 class Remuneration(models
.Model
):
549 dossier
= models
.ForeignKey('Dossier', db_column
='dossier')
550 type = models
.ForeignKey(rh
.TypeRemuneration
, db_column
='type',
553 # type_revalorisation = models.ForeignKey('TypeRevalorisation',
554 # db_column='type_revalorisation')
555 montant
= models
.DecimalField(max_digits
=12, decimal_places
=2,
557 devise
= models
.ForeignKey(rh
.Devise
, to_field
='code',
558 db_column
='devise', related_name
='+')
559 precision
= models
.CharField(max_length
=255, null
=True, blank
=True)
560 # date_effective = models.DateField(null=True, blank=True)
561 # pourcentage = models.IntegerField(null=True, blank=True)
564 date_creation
= models
.DateField(auto_now_add
=True)
565 user_creation
= models
.IntegerField(null
=True, blank
=True)
566 # desactivation = models.BooleanField(default=False, blank=True)
567 # date_desactivation = models.DateField(null=True, blank=True)
568 # user_desactivation = models.IntegerField(null=True, blank=True)
569 # annulation = models.BooleanField(default=False, blank=True)
570 # date_annulation = models.DateField(null=True, blank=True)
571 # user_annulation = models.IntegerField(null=True, blank=True)
573 def montant_mois(self
):
574 return round(self
.montant
/ 12, 2)
576 def taux_devise(self
):
577 return self
.devise
.tauxchange_set
.order_by('-annee').all()[0].taux
579 def montant_euro(self
):
580 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
582 def montant_euro_mois(self
):
583 return round(self
.montant_euro() / 12, 2)
586 TYPE_JUSTIFICATIONS
= (
587 ('N', 'Nouvel employé'),
588 ('R', 'Renouvellement employé'),
591 class JustificationQuestion(models
.Model
):
592 question
= models
.CharField(max_length
=255)
593 type = models
.CharField(max_length
=255, choices
=TYPE_JUSTIFICATIONS
)
595 def __unicode__(self
,):
598 class JustificationNouvelEmploye(models
.Model
):
599 dossier
= models
.ForeignKey("Dossier")
600 question
= models
.ForeignKey("JustificationQuestion")
601 reponse
= models
.TextField()
603 class JustificationAutreEmploye(models
.Model
):
604 dossier
= models
.ForeignKey("Dossier")
605 question
= models
.ForeignKey("JustificationQuestion")
606 reponse
= models
.TextField()