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 workflow
import DOSSIER_ETAT_DRH_FINALISATION
10 from managers
import DossierManager
, PosteManager
11 import datamaster_modeles
.models
as ref
12 from rh_v1
import models
as rh
16 HELP_TEXT_DATE
= "format: aaaa-mm-jj"
17 REGIME_TRAVAIL_DEFAULT
=100.00
18 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
=35.00
22 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
23 base_url
=settings
.PRIVE_MEDIA_URL
)
25 def poste_piece_dispatch(instance
, filename
):
26 path
= "poste/%s/%s" % (instance
.poste_id
, filename
)
29 def dossier_piece_dispatch(instance
, filename
):
30 path
= "dossier/%s/%s" % (instance
.dossier_id
, filename
)
36 POSTE_APPEL_CHOICES
= (
37 ('interne', 'Interne'),
38 ('externe', 'Externe'),
42 class Poste(PosteWorkflow
, models
.Model
):
44 id_rh
= models
.ForeignKey(rh
.Poste
, null
=True, related_name
='+',
46 verbose_name
="Mise à jour du poste")
47 nom
= models
.CharField(verbose_name
="Titre du poste", max_length
=255)
48 implantation
= models
.ForeignKey(ref
.Implantation
)
49 type_poste
= models
.ForeignKey(rh
.TypePoste
, null
=True, related_name
='+')
50 service
= models
.ForeignKey(rh
.Service
, related_name
='+',
51 verbose_name
=u
"Direction/Service/Pôle support")
52 responsable
= models
.ForeignKey(rh
.Poste
, related_name
='+',
53 verbose_name
="Poste du responsable")
56 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
57 default
=REGIME_TRAVAIL_DEFAULT
,
58 verbose_name
="Temps de travail",
59 help_text
="% du temps complet")
60 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
62 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
63 verbose_name
="Nb. heures par semaine")
66 local
= models
.BooleanField(verbose_name
="Local", default
=True, blank
=True)
67 expatrie
= models
.BooleanField(verbose_name
="Expatrié", default
=False,
69 mise_a_disposition
= models
.BooleanField(verbose_name
="Mise à disposition")
70 appel
= models
.CharField(max_length
=10, default
='interne',
71 verbose_name
="Appel à candidature",
72 choices
=POSTE_APPEL_CHOICES
)
75 classement_min
= models
.ForeignKey(rh
.Classement
, related_name
='+',
76 blank
=True, null
=True)
77 classement_max
= models
.ForeignKey(rh
.Classement
, related_name
='+',
78 blank
=True, null
=True)
79 valeur_point_min
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+',
80 blank
=True, null
=True)
81 valeur_point_max
= models
.ForeignKey(rh
.ValeurPoint
, related_name
='+',
82 blank
=True, null
=True)
83 devise_min
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
84 devise_max
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
85 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
87 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
89 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
91 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
93 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
95 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
98 # Comparatifs de rémunération
99 devise_comparaison
= models
.ForeignKey(rh
.Devise
, related_name
='+',
101 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
102 null
=True, blank
=True)
103 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
104 null
=True, blank
=True)
105 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
106 null
=True, blank
=True)
107 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
108 null
=True, blank
=True)
109 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
110 null
=True, blank
=True)
111 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
112 null
=True, blank
=True)
113 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
114 null
=True, blank
=True)
115 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
116 null
=True, blank
=True)
117 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
118 null
=True, blank
=True)
119 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
120 null
=True, blank
=True)
123 justification
= models
.TextField()
126 date_creation
= models
.DateTimeField(auto_now_add
=True)
127 date_modification
= models
.DateTimeField(auto_now
=True)
128 date_debut
= models
.DateField(verbose_name
="Date de début",
129 help_text
=HELP_TEXT_DATE
)
130 date_fin
= models
.DateField(null
=True, blank
=True,
131 verbose_name
="Date de fin",
132 help_text
=HELP_TEXT_DATE
)
133 actif
= models
.BooleanField(default
=True)
134 pourvu
= models
.BooleanField(default
=False)
137 objects
= PosteManager()
141 Les vues sont montées selon une clef spéciale
142 pour identifier la provenance du poste.
143 Cette méthode fournit un moyen de reconstruire cette clef
144 afin de générer les URLs.
146 return "dae-%s" % self
.id
147 key
= property(_get_key
)
149 def get_dossiers(self
):
151 Liste tous les anciens dossiers liés à ce poste.
152 (Le nom de la relation sur le rh.Poste est mal choisi
153 poste1 au lieu de dossier1)
154 Note1 : seulement le dosssier principal fait l'objet de la recherche.
155 Note2 : les dossiers sont retournés du plus récent au plus vieux.
156 (Ce test est fait en fonction du id,
157 car les dates de création sont absentes de rh v1).
159 if self
.id_rh
is None:
161 postes
= [p
for p
in self
.id_rh
.poste1
.all()]
162 return sorted(postes
, key
=lambda poste
: poste
.id, reverse
=True)
164 def get_complement_nom(self
):
166 Inspecte les modèles rh v1 pour trouver dans le dernier dossier
167 un complément de titre de poste.
169 dossiers
= self
.get_dossiers()
170 if len(dossiers
) > 0:
171 nom
= dossiers
[0].complement1
176 def get_employe(self
):
178 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
180 dossiers
= self
.get_dossiers()
181 if len(dossiers
) > 0:
182 return dossiers
[0].employe
186 def get_default_devise(self
):
187 """Récupère la devise par défaut en fonction de l'implantation
191 implantation_devise
= rh
.TauxChange
.objects \
192 .filter(implantation
=self
.implantation
)[0].devise
194 implantation_devise
= 5 # EUR
195 return implantation_devise
197 #####################
198 # Classement de poste
199 #####################
201 def get_couts_minimum(self
):
202 return (float)(self
.salaire_min
+ self
.indemn_min
+ self
.autre_min
)
204 def get_taux_minimum(self
):
205 taux_changes
= rh
.TauxChange
.objects
.filter(devise
=self
.devise_min
).order_by('annee')
206 for t
in taux_changes
:
207 if t
.implantation
== self
.implantation
:
209 if len(taux_changes
) > 0:
210 return taux_changes
[0].taux
212 raise Exception('Taux indisponible pour la devise %s (%s)' % (self
.devise_min
, self
.implantation
))
214 def get_couts_minimum_euros(self
):
215 return self
.get_couts_minimum() * self
.get_taux_minimum()
217 def get_couts_maximum(self
):
218 return (float)(self
.salaire_max
+ self
.indemn_max
+ self
.autre_max
)
220 def get_taux_maximum(self
):
221 taux_changes
= rh
.TauxChange
.objects
.filter(devise
=self
.devise_max
).order_by('annee')
222 for t
in taux_changes
:
223 if t
.implantation
== self
.implantation
:
225 if len(taux_changes
) > 0:
226 return taux_changes
[0].taux
228 raise Exception('Taux indisponible pour la devise %s (%s)' % (self
.devise_max
, self
.implantation
))
230 def get_couts_maximum_euros(self
):
231 return self
.get_couts_maximum() * self
.get_taux_maximum()
234 def show_taux_minimum(self
):
236 return self
.get_taux_minimum()
240 def show_couts_minimum_euros(self
):
242 return self
.get_couts_minimum_euros()
246 def show_taux_maximum(self
):
248 return self
.get_taux_maximum()
252 def show_couts_maximum_euros(self
):
254 return self
.get_couts_maximum_euros()
259 ######################
260 # Comparaison de poste
261 ######################
263 def est_comparable(self
):
265 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
268 if self
.comp_universite_min
is None and \
269 self
.comp_fonctionpub_min
is None and \
270 self
.comp_locale_min
is None and \
271 self
.comp_ong_min
is None and \
272 self
.comp_autre_min
is None and \
273 self
.comp_universite_max
is None and \
274 self
.comp_fonctionpub_max
is None and \
275 self
.comp_locale_max
is None and \
276 self
.comp_ong_max
is None and \
277 self
.comp_autre_max
is None:
283 def get_taux_comparaison(self
):
285 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_comparaison
)[0].taux
289 def get_comp_universite_min_euros(self
):
290 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
292 def get_comp_fonctionpub_min_euros(self
):
293 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
295 def get_comp_locale_min_euros(self
):
296 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
298 def get_comp_ong_min_euros(self
):
299 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
301 def get_comp_autre_min_euros(self
):
302 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
304 def get_comp_universite_max_euros(self
):
305 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
307 def get_comp_fonctionpub_max_euros(self
):
308 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
310 def get_comp_locale_max_euros(self
):
311 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
313 def get_comp_ong_max_euros(self
):
314 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
316 def get_comp_autre_max_euros(self
):
317 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
320 def __unicode__(self
):
322 Cette fonction est consommatrice SQL car elle cherche les dossiers
323 qui ont été liés à celui-ci.
325 complement_nom_poste
= self
.get_complement_nom()
326 if complement_nom_poste
is None:
327 complement_nom_poste
= ""
333 return u
'%s - %s (%s)' % data
336 # Tester l'enregistrement car les models.py sont importés au complet
337 if not reversion
.is_registered(Poste
):
338 reversion
.register(Poste
)
341 POSTE_FINANCEMENT_CHOICES
= (
342 ('A', 'A - Frais de personnel'),
343 ('B', 'B - Projet(s)-Titre(s)'),
347 class PosteFinancement(models
.Model
):
348 poste
= models
.ForeignKey('Poste', related_name
='financements')
349 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
350 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
351 help_text
="ex.: 33.33 % (décimale avec point)")
352 commentaire
= models
.TextField(
353 help_text
="Spécifiez la source de financement.")
358 def __unicode__(self
):
359 return u
"%s %s %s" % (self
.get_type_display(), self
.pourcentage
, self
.commentaire
)
362 class PostePiece(models
.Model
):
363 """Documents relatifs au Poste
364 Ex.: Description de poste
366 poste
= models
.ForeignKey("Poste")
367 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
368 fichier
= models
.FileField(verbose_name
="Fichier",
369 upload_to
=poste_piece_dispatch
,
370 storage
=storage_prive
)
374 # TODO : migration pour m -> M, f -> F
381 class Employe(models
.Model
):
384 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
385 verbose_name
='Employé')
386 nom
= models
.CharField(max_length
=255)
387 prenom
= models
.CharField(max_length
=255, verbose_name
='Prénom')
388 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
390 def __unicode__(self
):
391 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
396 STATUT_RESIDENCE_CHOICES
= (
398 ('expat', 'Expatrié'),
401 COMPTE_COMPTA_CHOICES
= (
407 class Dossier(DossierWorkflow
, models
.Model
):
410 employe
= models
.ForeignKey('Employe', related_name
='+', editable
=False)
411 poste
= models
.ForeignKey('Poste', related_name
='+', editable
=False)
412 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+')
413 organisme_bstg
= models
.ForeignKey(rh
.OrganismeBstg
,
414 null
=True, blank
=True,
415 verbose_name
="Organisme",
416 help_text
="Si détaché (DET) ou mis à disposition (MAD), \
417 préciser l'organisme.",
419 organisme_bstg_autre
= models
.CharField(max_length
=255,
420 verbose_name
="Autre organisme",
421 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
425 # Données antérieures de l'employé
426 statut_anterieur
= models
.ForeignKey(
427 rh
.Statut
, related_name
='+', null
=True, blank
=True,
428 verbose_name
='Statut antérieur')
429 classement_anterieur
= models
.ForeignKey(
430 rh
.Classement
, related_name
='+', null
=True, blank
=True,
431 verbose_name
='Classement précédent')
432 salaire_anterieur
= models
.DecimalField(
433 max_digits
=12, decimal_places
=2, null
=True, default
=None,
434 blank
=True, verbose_name
='Salaire précédent')
436 # Données du titulaire précédent
437 employe_anterieur
= models
.ForeignKey(
438 rh
.Employe
, related_name
='+', null
=True, blank
=True,
439 verbose_name
='Employé précédent')
440 statut_titulaire_anterieur
= models
.ForeignKey(
441 rh
.Statut
, related_name
='+', null
=True, blank
=True,
442 verbose_name
='Statut titulaire précédent')
443 classement_titulaire_anterieur
= models
.ForeignKey(
444 rh
.Classement
, related_name
='+', null
=True, blank
=True,
445 verbose_name
='Classement titulaire précédent')
446 salaire_titulaire_anterieur
= models
.DecimalField(
447 max_digits
=12, decimal_places
=2, default
=None, null
=True,
448 blank
=True, verbose_name
='Salaire titulaire précédent')
451 remplacement
= models
.BooleanField()
452 statut_residence
= models
.CharField(max_length
=10, default
='local',
453 verbose_name
="Statut",
454 choices
=STATUT_RESIDENCE_CHOICES
)
457 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+',
458 null
=True, blank
=True,
459 verbose_name
='Classement proposé')
460 salaire
= models
.DecimalField(max_digits
=12, decimal_places
=2,
461 verbose_name
='Salaire de base',
462 null
=True, default
=None)
463 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
464 regime_travail
= models
.DecimalField(max_digits
=12,
466 default
=REGIME_TRAVAIL_DEFAULT
,
467 verbose_name
="Régime de travail",
468 help_text
="% du temps complet")
469 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
471 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
472 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)
490 objects
= DossierManager()
492 def __unicode__(self
):
493 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
495 def get_salaire_euros(self
):
497 tx
= rh
.TauxChange
.objects
.filter(implantation
=self
.poste
.implantation
, devise
=self
.devise
)[0].taux
500 return (float)(tx
) * (float)(self
.salaire
)
502 def get_couts_auf(self
):
504 On retire les MAD BSTG
506 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
not in (2, )]
508 def get_total_couts_auf(self
):
510 for r
in self
.get_couts_auf():
511 total
+= r
.montant_euro()
514 def get_aides_auf(self
):
516 On récupère les MAD BSTG
518 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
in (2, )]
520 def get_total_aides_auf(self
):
522 for r
in self
.get_aides_auf():
523 total
+= r
.montant_euro()
526 def get_total_remun(self
):
527 return self
.get_total_couts_auf() + self
.get_total_aides_auf()
529 # Tester l'enregistrement car les models.py sont importés au complet
530 if not reversion
.is_registered(Dossier
):
531 reversion
.register(Dossier
)
533 class DossierPiece(models
.Model
):
534 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
535 Ex.: Lettre de motivation.
537 dossier
= models
.ForeignKey("Dossier")
538 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
539 fichier
= models
.FileField(verbose_name
="Fichier",
540 upload_to
=dossier_piece_dispatch
,
541 storage
=storage_prive
)
544 class DossierComparaison(models
.Model
):
546 Photo d'une comparaison salariale au moment de l'embauche.
548 dossier
= models
.ForeignKey('Dossier', related_name
='comparaisons')
549 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True)
550 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
551 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
552 montant
= models
.IntegerField(null
=True)
553 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+', null
=True, blank
=True)
554 montant_euros
= models
.IntegerField(null
=True)
559 class Remuneration(models
.Model
):
561 dossier
= models
.ForeignKey('Dossier', db_column
='dossier')
562 type = models
.ForeignKey(rh
.TypeRemuneration
, db_column
='type',
564 montant
= models
.DecimalField(max_digits
=12, decimal_places
=2,
566 devise
= models
.ForeignKey(rh
.Devise
, to_field
='code',
567 db_column
='devise', related_name
='+')
568 precision
= models
.CharField(max_length
=255, null
=True, blank
=True)
571 date_creation
= models
.DateField(auto_now_add
=True)
572 user_creation
= models
.IntegerField(null
=True, blank
=True) # TODO : user
574 def montant_mois(self
):
575 return round(self
.montant
/ 12, 2)
577 def taux_devise(self
):
578 return self
.devise
.tauxchange_set
.order_by('-annee').all()[0].taux
580 def montant_euro(self
):
581 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
583 def montant_euro_mois(self
):
584 return round(self
.montant_euro() / 12, 2)
589 TYPE_JUSTIFICATIONS
= (
590 ('N', 'Nouvel employé'),
591 ('R', 'Renouvellement employé'),
594 class JustificationQuestion(models
.Model
):
595 question
= models
.CharField(max_length
=255)
596 type = models
.CharField(max_length
=255, choices
=TYPE_JUSTIFICATIONS
)
598 def __unicode__(self
,):
601 class JustificationNouvelEmploye(models
.Model
):
602 dossier
= models
.ForeignKey("Dossier")
603 question
= models
.ForeignKey("JustificationQuestion")
604 reponse
= models
.TextField()
606 class JustificationAutreEmploye(models
.Model
):
607 dossier
= models
.ForeignKey("Dossier")
608 question
= models
.ForeignKey("JustificationQuestion")
609 reponse
= models
.TextField()