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
):
203 taux_changes
= rh
.TauxChange
.objects
.filter(devise
=self
.devise_min
).order_by('annee')
204 for t
in taux_changes
:
205 if t
.implantation
== self
.implantation
:
207 if len(taux_changes
) > 0:
208 return taux_changes
[0].taux
210 raise Exception('Taux indisponible pour la devise %s (%s)' % (self
.devise_min
, self
.implantation
))
212 def get_couts_minimum_euros(self
):
213 return self
.get_couts_minimum() * self
.get_taux_minimum()
215 def get_couts_maximum(self
):
216 return (float)(self
.salaire_max
+ self
.indemn_max
+ self
.autre_max
)
218 def get_taux_maximum(self
):
219 taux_changes
= rh
.TauxChange
.objects
.filter(devise
=self
.devise_max
).order_by('annee')
220 for t
in taux_changes
:
221 if t
.implantation
== self
.implantation
:
223 if len(taux_changes
) > 0:
224 return taux_changes
[0].taux
226 raise Exception('Taux indisponible pour la devise %s (%s)' % (self
.devise_max
, self
.implantation
))
228 def get_couts_maximum_euros(self
):
229 return self
.get_couts_maximum() * self
.get_taux_maximum()
232 def show_taux_minimum(self
):
234 return self
.get_taux_minimum()
238 def show_couts_minimum_euros(self
):
240 return self
.get_couts_minimum_euros()
244 def show_taux_maximum(self
):
246 return self
.get_taux_maximum()
250 def show_couts_maximum_euros(self
):
252 return self
.get_couts_maximum_euros()
257 ######################
258 # Comparaison de poste
259 ######################
261 def est_comparable(self
):
263 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
266 if self
.comp_universite_min
is None and \
267 self
.comp_fonctionpub_min
is None and \
268 self
.comp_locale_min
is None and \
269 self
.comp_ong_min
is None and \
270 self
.comp_autre_min
is None and \
271 self
.comp_universite_max
is None and \
272 self
.comp_fonctionpub_max
is None and \
273 self
.comp_locale_max
is None and \
274 self
.comp_ong_max
is None and \
275 self
.comp_autre_max
is None:
281 def get_taux_comparaison(self
):
283 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_comparaison
)[0].taux
287 def get_comp_universite_min_euros(self
):
288 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
290 def get_comp_fonctionpub_min_euros(self
):
291 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
293 def get_comp_locale_min_euros(self
):
294 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
296 def get_comp_ong_min_euros(self
):
297 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
299 def get_comp_autre_min_euros(self
):
300 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
302 def get_comp_universite_max_euros(self
):
303 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
305 def get_comp_fonctionpub_max_euros(self
):
306 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
308 def get_comp_locale_max_euros(self
):
309 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
311 def get_comp_ong_max_euros(self
):
312 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
314 def get_comp_autre_max_euros(self
):
315 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
318 def __unicode__(self
):
320 Cette fonction est consommatrice SQL car elle cherche les dossiers
321 qui ont été liés à celui-ci.
323 complement_nom_poste
= self
.get_complement_nom()
324 if complement_nom_poste
is None:
325 complement_nom_poste
= ""
331 return u
'%s - %s (%s)' % data
334 # Tester l'enregistrement car les models.py sont importés au complet
335 if not reversion
.is_registered(Poste
):
336 reversion
.register(Poste
)
339 POSTE_FINANCEMENT_CHOICES
= (
340 ('A', 'A - Frais de personnel'),
341 ('B', 'B - Projet(s)-Titre(s)'),
345 class PosteFinancement(models
.Model
):
346 poste
= models
.ForeignKey('Poste', related_name
='financements')
347 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
348 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
349 help_text
="ex.: 33.33 % (décimale avec point)")
350 commentaire
= models
.TextField(
351 help_text
="Spécifiez la source de financement.")
356 def __unicode__(self
):
357 return u
"%s %s %s" % (self
.get_type_display(), self
.pourcentage
, self
.commentaire
)
360 class PostePiece(models
.Model
):
361 """Documents relatifs au Poste
362 Ex.: Description de poste
364 poste
= models
.ForeignKey("Poste")
365 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
366 fichier
= models
.FileField(verbose_name
="Fichier",
367 upload_to
=poste_piece_dispatch
,
368 storage
=storage_prive
)
372 # TODO : migration pour m -> M, f -> F
379 class Employe(models
.Model
):
382 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
383 verbose_name
='Employé')
384 nom
= models
.CharField(max_length
=255)
385 prenom
= models
.CharField(max_length
=255, verbose_name
='Prénom')
386 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
388 def __unicode__(self
):
389 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
394 STATUT_RESIDENCE_CHOICES
= (
396 ('expat', 'Expatrié'),
399 COMPTE_COMPTA_CHOICES
= (
405 class Dossier(DossierWorkflow
, models
.Model
):
408 employe
= models
.ForeignKey('Employe', related_name
='+', editable
=False)
409 poste
= models
.ForeignKey('Poste', related_name
='+', editable
=False)
410 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+')
411 organisme_bstg
= models
.ForeignKey(rh
.OrganismeBstg
,
412 null
=True, blank
=True,
413 verbose_name
="Organisme",
414 help_text
="Si détaché (DET) ou mis à disposition (MAD), \
415 préciser l'organisme.",
417 organisme_bstg_autre
= models
.CharField(max_length
=255,
418 verbose_name
="Autre organisme",
419 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
423 # Données antérieures de l'employé
424 statut_anterieur
= models
.ForeignKey(
425 rh
.Statut
, related_name
='+', null
=True, blank
=True,
426 verbose_name
='Statut antérieur')
427 classement_anterieur
= models
.ForeignKey(
428 rh
.Classement
, related_name
='+', null
=True, blank
=True,
429 verbose_name
='Classement précédent')
430 salaire_anterieur
= models
.DecimalField(
431 max_digits
=12, decimal_places
=2, null
=True, default
=None,
432 blank
=True, verbose_name
='Salaire précédent')
434 # Données du titulaire précédent
435 employe_anterieur
= models
.ForeignKey(
436 rh
.Employe
, related_name
='+', null
=True, blank
=True,
437 verbose_name
='Employé précédent')
438 statut_titulaire_anterieur
= models
.ForeignKey(
439 rh
.Statut
, related_name
='+', null
=True, blank
=True,
440 verbose_name
='Statut titulaire précédent')
441 classement_titulaire_anterieur
= models
.ForeignKey(
442 rh
.Classement
, related_name
='+', null
=True, blank
=True,
443 verbose_name
='Classement titulaire précédent')
444 salaire_titulaire_anterieur
= models
.DecimalField(
445 max_digits
=12, decimal_places
=2, default
=None, null
=True,
446 blank
=True, verbose_name
='Salaire titulaire précédent')
449 remplacement
= models
.BooleanField()
450 statut_residence
= models
.CharField(max_length
=10, default
='local',
451 verbose_name
="Statut",
452 choices
=STATUT_RESIDENCE_CHOICES
)
455 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+',
456 null
=True, blank
=True,
457 verbose_name
='Classement proposé')
458 salaire
= models
.DecimalField(max_digits
=12, decimal_places
=2,
459 verbose_name
='Salaire de base',
460 null
=True, default
=None)
461 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
462 regime_travail
= models
.DecimalField(max_digits
=12,
464 default
=REGIME_TRAVAIL_DEFAULT
,
465 verbose_name
="Régime de travail",
466 help_text
="% du temps complet")
467 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
469 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
470 verbose_name
="Nb. heures par semaine")
473 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
474 contrat_date_debut
= models
.DateField(help_text
="format: aaaa-mm-jj")
475 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
476 help_text
="format: aaaa-mm-jj")
479 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
480 verbose_name
=u
'Compte comptabilité',
481 choices
=COMPTE_COMPTA_CHOICES
)
482 compte_courriel
= models
.BooleanField()
485 date_creation
= models
.DateTimeField(auto_now_add
=True)
488 objects
= DossierManager()
490 def __unicode__(self
):
491 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
493 def get_salaire_euros(self
):
495 tx
= rh
.TauxChange
.objects
.filter(implantation
=self
.poste
.implantation
, devise
=self
.devise
)[0].taux
498 return (float)(tx
) * (float)(self
.salaire
)
500 def get_couts_auf(self
):
502 On retire les MAD BSTG
504 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
not in (2, )]
506 def get_total_couts_auf(self
):
508 for r
in self
.get_couts_auf():
509 total
+= r
.montant_euro()
512 def get_aides_auf(self
):
514 On récupère les MAD BSTG
516 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
in (2, )]
518 def get_total_aides_auf(self
):
520 for r
in self
.get_aides_auf():
521 total
+= r
.montant_euro()
524 def get_total_remun(self
):
525 return self
.get_total_couts_auf() + self
.get_total_aides_auf()
527 # Tester l'enregistrement car les models.py sont importés au complet
528 if not reversion
.is_registered(Dossier
):
529 reversion
.register(Dossier
)
531 class DossierPiece(models
.Model
):
532 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
533 Ex.: Lettre de motivation.
535 dossier
= models
.ForeignKey("Dossier")
536 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
537 fichier
= models
.FileField(verbose_name
="Fichier",
538 upload_to
=dossier_piece_dispatch
,
539 storage
=storage_prive
)
542 class DossierComparaison(models
.Model
):
544 Photo d'une comparaison salariale au moment de l'embauche.
546 dossier
= models
.ForeignKey('Dossier', related_name
='comparaisons')
547 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True)
548 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
549 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
550 montant
= models
.IntegerField(null
=True)
551 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+', null
=True, blank
=True)
552 montant_euros
= models
.IntegerField(null
=True)
557 class Remuneration(models
.Model
):
559 dossier
= models
.ForeignKey('Dossier', db_column
='dossier')
560 type = models
.ForeignKey(rh
.TypeRemuneration
, db_column
='type',
562 montant
= models
.DecimalField(max_digits
=12, decimal_places
=2,
564 devise
= models
.ForeignKey(rh
.Devise
, to_field
='code',
565 db_column
='devise', related_name
='+')
566 precision
= models
.CharField(max_length
=255, null
=True, blank
=True)
569 date_creation
= models
.DateField(auto_now_add
=True)
570 user_creation
= models
.IntegerField(null
=True, blank
=True) # TODO : user
572 def montant_mois(self
):
573 return round(self
.montant
/ 12, 2)
575 def taux_devise(self
):
576 return self
.devise
.tauxchange_set
.order_by('-annee').all()[0].taux
578 def montant_euro(self
):
579 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
581 def montant_euro_mois(self
):
582 return round(self
.montant_euro() / 12, 2)
587 TYPE_JUSTIFICATIONS
= (
588 ('N', 'Nouvel employé'),
589 ('R', 'Renouvellement employé'),
592 class JustificationQuestion(models
.Model
):
593 question
= models
.CharField(max_length
=255)
594 type = models
.CharField(max_length
=255, choices
=TYPE_JUSTIFICATIONS
)
596 def __unicode__(self
,):
599 class JustificationNouvelEmploye(models
.Model
):
600 dossier
= models
.ForeignKey("Dossier")
601 question
= models
.ForeignKey("JustificationQuestion")
602 reponse
= models
.TextField()
604 class JustificationAutreEmploye(models
.Model
):
605 dossier
= models
.ForeignKey("Dossier")
606 question
= models
.ForeignKey("JustificationQuestion")
607 reponse
= models
.TextField()