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 workflow
import PosteWorkflow
, DossierWorkflow
9 from managers
import DossierManager
, PosteManager
10 import datamaster_modeles
.models
as ref
11 from rh_v1
import models
as rh
15 HELP_TEXT_DATE
= "format: aaaa-mm-jj"
16 REGIME_TRAVAIL_DEFAULT
=100.00
17 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
=35.00
21 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
22 base_url
=settings
.PRIVE_MEDIA_URL
)
24 def poste_piece_dispatch(instance
, filename
):
25 path
= "poste/%s/%s" % (instance
.poste_id
, filename
)
28 def dossier_piece_dispatch(instance
, filename
):
29 path
= "dossier/%s/%s" % (instance
.dossier_id
, filename
)
35 POSTE_APPEL_CHOICES
= (
36 ('interne', 'Interne'),
37 ('externe', 'Externe'),
41 class Poste(PosteWorkflow
, models
.Model
):
43 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
45 verbose_name
="Mise à jour du poste")
46 nom
= models
.CharField(verbose_name
="Titre du poste", max_length
=255)
47 implantation
= models
.ForeignKey(ref
.Implantation
)
48 type_poste
= models
.ForeignKey(rh
.TypePoste
, null
=True, related_name
='+')
49 service
= models
.ForeignKey(rh
.Service
, related_name
='+',
50 verbose_name
=u
"Direction/Service/Pôle support")
51 responsable
= models
.ForeignKey(rh
.Poste
, related_name
='+',
52 verbose_name
="Poste du responsable")
55 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
56 default
=REGIME_TRAVAIL_DEFAULT
,
57 verbose_name
="Temps de travail",
58 help_text
="% du temps complet")
59 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
61 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
62 verbose_name
="Nb. heures par semaine")
65 local
= models
.BooleanField(verbose_name
="Local", default
=True, blank
=True)
66 expatrie
= models
.BooleanField(verbose_name
="Expatrié", default
=False,
68 mise_a_disposition
= models
.BooleanField(verbose_name
="Mise à disposition")
69 appel
= models
.CharField(max_length
=10, default
='interne',
70 verbose_name
="Appel à candidature",
71 choices
=POSTE_APPEL_CHOICES
)
74 classement_min
= models
.ForeignKey(rh
.Classement
, related_name
='+',
75 blank
=True, null
=True)
76 classement_max
= models
.ForeignKey(rh
.Classement
, related_name
='+',
77 blank
=True, null
=True)
78 valeur_point_min
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+',
79 blank
=True, null
=True)
80 valeur_point_max
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+',
81 blank
=True, null
=True)
82 devise_min
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
83 devise_max
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
84 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
86 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
88 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
90 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
92 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
94 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
97 # Comparatifs de rémunération
98 devise_comparaison
= models
.ForeignKey(rh
.Devise
, related_name
='+',
100 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
101 null
=True, blank
=True)
102 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
103 null
=True, blank
=True)
104 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
105 null
=True, blank
=True)
106 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
107 null
=True, blank
=True)
108 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
109 null
=True, blank
=True)
110 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
111 null
=True, blank
=True)
112 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
113 null
=True, blank
=True)
114 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
115 null
=True, blank
=True)
116 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
117 null
=True, blank
=True)
118 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
119 null
=True, blank
=True)
122 justification
= models
.TextField()
125 date_creation
= models
.DateTimeField(auto_now_add
=True)
126 date_modification
= models
.DateTimeField(auto_now
=True)
127 date_debut
= models
.DateField(verbose_name
="Date de début",
128 help_text
=HELP_TEXT_DATE
)
129 date_fin
= models
.DateField(null
=True, blank
=True,
130 verbose_name
="Date de fin",
131 help_text
=HELP_TEXT_DATE
)
132 actif
= models
.BooleanField(default
=True)
135 objects
= PosteManager()
139 Les vues sont montées selon une clef spéciale
140 pour identifier la provenance du poste.
141 Cette méthode fournit un moyen de reconstruire cette clef
142 afin de générer les URLs.
144 return "dae-%s" % self
.id
145 key
= property(_get_key
)
147 def get_dossiers(self
):
149 Liste tous les anciens dossiers liés à ce poste.
150 (Le nom de la relation sur le rh.Poste est mal choisi
151 poste1 au lieu de dossier1)
152 Note1 : seulement le dosssier principal fait l'objet de la recherche.
153 Note2 : les dossiers sont retournés du plus récent au plus vieux.
154 (Ce test est fait en fonction du id,
155 car les dates de création sont absentes de rh v1).
157 if self
.id_rh
is None:
159 postes
= [p
for p
in self
.id_rh
.poste1
.all()]
160 return sorted(postes
, key
=lambda poste
: poste
.id, reverse
=True)
162 def get_complement_nom(self
):
164 Inspecte les modèles rh v1 pour trouver dans le dernier dossier
165 un complément de titre de poste.
167 dossiers
= self
.get_dossiers()
168 if len(dossiers
) > 0:
169 nom
= dossiers
[0].complement1
174 def get_employe(self
):
176 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
178 dossiers
= self
.get_dossiers()
179 if len(dossiers
) > 0:
180 return dossiers
[0].employe
184 def get_default_devise(self
):
185 """Récupère la devise par défaut en fonction de l'implantation
189 implantation_devise
= rh
.TauxChange
.objects \
190 .filter(implantation
=self
.implantation
)[0].devise
192 implantation_devise
= 5 # EUR
193 return implantation_devise
195 #####################
196 # Classement de poste
197 #####################
199 def get_couts_minimum(self
):
200 return (float)(self
.salaire_min
+ self
.indemn_min
+ self
.autre_min
)
202 def get_taux_minimum(self
):
204 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_min
)[0].taux
206 raise Exception('Taux indisponible pour la devise %s (%s)' % (self
.devise_min
, self
.implantation
))
208 def get_couts_minimum_euros(self
):
209 return self
.get_couts_minimum() * self
.get_taux_minimum()
211 def get_couts_maximum(self
):
212 return (float)(self
.salaire_max
+ self
.indemn_max
+ self
.autre_max
)
214 def get_taux_maximum(self
):
216 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_max
)[0].taux
218 raise Exception('Taux indisponible pour la devise %s (%s)' % (self
.devise_max
, self
.implantation
))
220 def get_couts_maximum_euros(self
):
221 return self
.get_couts_maximum() * self
.get_taux_maximum()
224 def show_taux_minimum(self
):
226 return self
.get_taux_minimum()
230 def show_couts_minimum_euros(self
):
232 return self
.get_couts_minimum_euros()
236 def show_taux_maximum(self
):
238 return self
.get_taux_maximum()
242 def show_couts_maximum_euros(self
):
244 return self
.get_couts_maximum_euros()
249 ######################
250 # Comparaison de poste
251 ######################
253 def est_comparable(self
):
255 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
258 if self
.comp_universite_min
is None and \
259 self
.comp_fonctionpub_min
is None and \
260 self
.comp_locale_min
is None and \
261 self
.comp_ong_min
is None and \
262 self
.comp_autre_min
is None and \
263 self
.comp_universite_max
is None and \
264 self
.comp_fonctionpub_max
is None and \
265 self
.comp_locale_max
is None and \
266 self
.comp_ong_max
is None and \
267 self
.comp_autre_max
is None:
273 def get_taux_comparaison(self
):
275 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_comparaison
)[0].taux
279 def get_comp_universite_min_euros(self
):
280 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
282 def get_comp_fonctionpub_min_euros(self
):
283 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
285 def get_comp_locale_min_euros(self
):
286 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
288 def get_comp_ong_min_euros(self
):
289 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
291 def get_comp_autre_min_euros(self
):
292 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
294 def get_comp_universite_max_euros(self
):
295 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
297 def get_comp_fonctionpub_max_euros(self
):
298 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
300 def get_comp_locale_max_euros(self
):
301 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
303 def get_comp_ong_max_euros(self
):
304 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
306 def get_comp_autre_max_euros(self
):
307 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
310 def __unicode__(self
):
312 Cette fonction est consommatrice SQL car elle cherche les dossiers
313 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
= ""
323 return u
'%s - %s (%s)' % data
326 # Tester l'enregistrement car les models.py sont importés au complet
327 if not reversion
.is_registered(Poste
):
328 reversion
.register(Poste
)
331 POSTE_FINANCEMENT_CHOICES
= (
332 ('A', 'A - Frais de personnel'),
333 ('B', 'B - Projet(s)-Titre(s)'),
337 class PosteFinancement(models
.Model
):
338 poste
= models
.ForeignKey('Poste', related_name
='financements')
339 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
340 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
341 help_text
="ex.: 33.33 % (décimale avec point)")
342 commentaire
= models
.TextField(
343 help_text
="Spécifiez la source de financement.")
348 def __unicode__(self
):
349 return u
"%s %s %s" % (self
.get_type_display(), self
.pourcentage
, self
.commentaire
)
352 class PostePiece(models
.Model
):
353 """Documents relatifs au Poste
354 Ex.: Description de poste
356 poste
= models
.ForeignKey("Poste")
357 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
358 fichier
= models
.FileField(verbose_name
="Fichier",
359 upload_to
=poste_piece_dispatch
,
360 storage
=storage_prive
)
364 # TODO : migration pour m -> M, f -> F
371 class Employe(models
.Model
):
374 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
375 verbose_name
='Employé')
376 nom
= models
.CharField(max_length
=255)
377 prenom
= models
.CharField(max_length
=255, verbose_name
='Prénom')
378 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
380 def __unicode__(self
):
381 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
386 STATUT_RESIDENCE_CHOICES
= (
388 ('expat', 'Expatrié'),
391 COMPTE_COMPTA_CHOICES
= (
397 class Dossier(DossierWorkflow
, models
.Model
):
400 employe
= models
.ForeignKey('Employe', related_name
='+', editable
=False)
401 poste
= models
.ForeignKey('Poste', related_name
='+', editable
=False)
402 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+')
403 organisme_bstg
= models
.ForeignKey(rh
.OrganismeBstg
,
404 null
=True, blank
=True,
405 verbose_name
="Organisme",
406 help_text
="Si détaché (DET) ou mis à disposition (MAD), \
407 préciser l'organisme.",
409 organisme_bstg_autre
= models
.CharField(max_length
=255,
410 verbose_name
="Autre organisme",
411 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
415 # Données antérieures de l'employé
416 statut_anterieur
= models
.ForeignKey(
417 rh
.Statut
, related_name
='+', null
=True, blank
=True,
418 verbose_name
='Statut antérieur')
419 classement_anterieur
= models
.ForeignKey(
420 rh
.Classement
, related_name
='+', null
=True, blank
=True,
421 verbose_name
='Classement précédent')
422 salaire_anterieur
= models
.DecimalField(
423 max_digits
=12, decimal_places
=2, null
=True, default
=None,
424 blank
=True, verbose_name
='Salaire précédent')
426 # Données du titulaire précédent
427 employe_anterieur
= models
.ForeignKey(
428 rh
.Employe
, related_name
='+', null
=True, blank
=True,
429 verbose_name
='Employé précédent')
430 statut_titulaire_anterieur
= models
.ForeignKey(
431 rh
.Statut
, related_name
='+', null
=True, blank
=True,
432 verbose_name
='Statut titulaire précédent')
433 classement_titulaire_anterieur
= models
.ForeignKey(
434 rh
.Classement
, related_name
='+', null
=True, blank
=True,
435 verbose_name
='Classement titulaire précédent')
436 salaire_titulaire_anterieur
= models
.DecimalField(
437 max_digits
=12, decimal_places
=2, default
=None, null
=True,
438 blank
=True, verbose_name
='Salaire titulaire précédent')
441 remplacement
= models
.BooleanField()
442 statut_residence
= models
.CharField(max_length
=10, default
='local',
443 verbose_name
="Statut",
444 choices
=STATUT_RESIDENCE_CHOICES
)
447 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+',
448 null
=True, blank
=True,
449 verbose_name
='Classement proposé')
450 salaire
= models
.DecimalField(max_digits
=12, decimal_places
=2,
451 verbose_name
='Salaire de base',
452 null
=True, default
=None)
453 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
454 regime_travail
= models
.DecimalField(max_digits
=12,
456 default
=REGIME_TRAVAIL_DEFAULT
,
457 verbose_name
="Régime de travail",
458 help_text
="% du temps complet")
459 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
461 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
462 verbose_name
="Nb. heures par semaine")
465 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
466 contrat_date_debut
= models
.DateField(help_text
="format: aaaa-mm-jj")
467 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
468 help_text
="format: aaaa-mm-jj")
471 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
472 verbose_name
=u
'Compte comptabilité',
473 choices
=COMPTE_COMPTA_CHOICES
)
474 compte_courriel
= models
.BooleanField()
477 date_creation
= models
.DateTimeField(auto_now_add
=True)
480 objects
= DossierManager()
482 def __unicode__(self
):
483 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
485 def get_salaire_euros(self
):
487 tx
= rh
.TauxChange
.objects
.filter(implantation
=self
.poste
.implantation
, devise
=self
.devise
)[0].taux
490 return (float)(tx
) * (float)(self
.salaire
)
492 def get_couts_auf(self
):
494 On retire les MAD BSTG
496 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
not in (2, )]
498 def get_total_couts_auf(self
):
500 for r
in self
.get_couts_auf():
501 total
+= r
.montant_euro()
504 def get_aides_auf(self
):
506 On récupère les MAD BSTG
508 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
in (2, )]
510 def get_total_aides_auf(self
):
512 for r
in self
.get_aides_auf():
513 total
+= r
.montant_euro()
516 def get_total_remun(self
):
517 return self
.get_total_couts_auf() + self
.get_total_aides_auf()
519 # Tester l'enregistrement car les models.py sont importés au complet
520 if not reversion
.is_registered(Dossier
):
521 reversion
.register(Dossier
)
523 class DossierPiece(models
.Model
):
524 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
525 Ex.: Lettre de motivation.
527 dossier
= models
.ForeignKey("Dossier")
528 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
529 fichier
= models
.FileField(verbose_name
="Fichier",
530 upload_to
=dossier_piece_dispatch
,
531 storage
=storage_prive
)
534 class DossierComparaison(models
.Model
):
536 Photo d'une comparaison salariale au moment de l'embauche.
538 dossier
= models
.ForeignKey('Dossier', related_name
='comparaisons')
539 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True)
540 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
541 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
542 montant
= models
.IntegerField(null
=True)
543 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+', null
=True, blank
=True)
544 montant_euros
= models
.IntegerField(null
=True)
549 class Remuneration(models
.Model
):
551 dossier
= models
.ForeignKey('Dossier', db_column
='dossier')
552 type = models
.ForeignKey(rh
.TypeRemuneration
, db_column
='type',
554 montant
= models
.DecimalField(max_digits
=12, decimal_places
=2,
556 devise
= models
.ForeignKey(rh
.Devise
, to_field
='code',
557 db_column
='devise', related_name
='+')
558 precision
= models
.CharField(max_length
=255, null
=True, blank
=True)
561 date_creation
= models
.DateField(auto_now_add
=True)
562 user_creation
= models
.IntegerField(null
=True, blank
=True) # TODO : user
564 def montant_mois(self
):
565 return round(self
.montant
/ 12, 2)
567 def taux_devise(self
):
568 return self
.devise
.tauxchange_set
.order_by('-annee').all()[0].taux
570 def montant_euro(self
):
571 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
573 def montant_euro_mois(self
):
574 return round(self
.montant_euro() / 12, 2)
579 TYPE_JUSTIFICATIONS
= (
580 ('N', 'Nouvel employé'),
581 ('R', 'Renouvellement employé'),
584 class JustificationQuestion(models
.Model
):
585 question
= models
.CharField(max_length
=255)
586 type = models
.CharField(max_length
=255, choices
=TYPE_JUSTIFICATIONS
)
588 def __unicode__(self
,):
591 class JustificationNouvelEmploye(models
.Model
):
592 dossier
= models
.ForeignKey("Dossier")
593 question
= models
.ForeignKey("JustificationQuestion")
594 reponse
= models
.TextField()
596 class JustificationAutreEmploye(models
.Model
):
597 dossier
= models
.ForeignKey("Dossier")
598 question
= models
.ForeignKey("JustificationQuestion")
599 reponse
= models
.TextField()