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
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
220 def get_couts_maximum_euros(self
):
221 return self
.get_couts_maximum() * self
.get_taux_maximum()
223 ######################
224 # Comparaison de poste
225 ######################
227 def est_comparable(self
):
229 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
232 if self
.comp_universite_min
is None and \
233 self
.comp_fonctionpub_min
is None and \
234 self
.comp_locale_min
is None and \
235 self
.comp_ong_min
is None and \
236 self
.comp_autre_min
is None and \
237 self
.comp_universite_max
is None and \
238 self
.comp_fonctionpub_max
is None and \
239 self
.comp_locale_max
is None and \
240 self
.comp_ong_max
is None and \
241 self
.comp_autre_max
is None:
247 def get_taux_comparaison(self
):
249 return rh
.TauxChange
.objects
.filter(implantation
=self
.implantation
, devise
=self
.devise_comparaison
)[0].taux
253 def get_comp_universite_min_euros(self
):
254 return (float)(self
.comp_universite_min
) * self
.get_taux_comparaison()
256 def get_comp_fonctionpub_min_euros(self
):
257 return (float)(self
.comp_fonctionpub_min
) * self
.get_taux_comparaison()
259 def get_comp_locale_min_euros(self
):
260 return (float)(self
.comp_locale_min
) * self
.get_taux_comparaison()
262 def get_comp_ong_min_euros(self
):
263 return (float)(self
.comp_ong_min
) * self
.get_taux_comparaison()
265 def get_comp_autre_min_euros(self
):
266 return (float)(self
.comp_autre_min
) * self
.get_taux_comparaison()
268 def get_comp_universite_max_euros(self
):
269 return (float)(self
.comp_universite_max
) * self
.get_taux_comparaison()
271 def get_comp_fonctionpub_max_euros(self
):
272 return (float)(self
.comp_fonctionpub_max
) * self
.get_taux_comparaison()
274 def get_comp_locale_max_euros(self
):
275 return (float)(self
.comp_locale_max
) * self
.get_taux_comparaison()
277 def get_comp_ong_max_euros(self
):
278 return (float)(self
.comp_ong_max
) * self
.get_taux_comparaison()
280 def get_comp_autre_max_euros(self
):
281 return (float)(self
.comp_autre_max
) * self
.get_taux_comparaison()
284 def __unicode__(self
):
286 Cette fonction est consommatrice SQL car elle cherche les dossiers
287 qui ont été liés à celui-ci.
289 complement_nom_poste
= self
.get_complement_nom()
290 if complement_nom_poste
is None:
291 complement_nom_poste
= ""
297 return u
'%s - %s (%s)' % data
300 # Tester l'enregistrement car les models.py sont importés au complet
301 if not reversion
.is_registered(Poste
):
302 reversion
.register(Poste
)
305 POSTE_FINANCEMENT_CHOICES
= (
306 ('A', 'A - Frais de personnel'),
307 ('B', 'B - Projet(s)-Titre(s)'),
311 class PosteFinancement(models
.Model
):
312 poste
= models
.ForeignKey('Poste', related_name
='financements')
313 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
314 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
315 help_text
="ex.: 33.33 % (décimale avec point)")
316 commentaire
= models
.TextField(
317 help_text
="Spécifiez la source de financement.")
322 def __unicode__(self
):
323 return u
"%s %s %s" % (self
.get_type_display(), self
.pourcentage
, self
.commentaire
)
326 class PostePiece(models
.Model
):
327 """Documents relatifs au Poste
328 Ex.: Description de poste
330 poste
= models
.ForeignKey("Poste")
331 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
332 fichier
= models
.FileField(verbose_name
="Fichier",
333 upload_to
=poste_piece_dispatch
,
334 storage
=storage_prive
)
338 # TODO : migration pour m -> M, f -> F
345 class Employe(models
.Model
):
348 id_rh
= models
.ForeignKey(rh
.Employe
, null
=True, related_name
='+',
349 verbose_name
='Employé')
350 nom
= models
.CharField(max_length
=255)
351 prenom
= models
.CharField(max_length
=255, verbose_name
='Prénom')
352 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
354 def __unicode__(self
):
355 return u
'%s %s' % (self
.prenom
, self
.nom
.upper())
360 STATUT_RESIDENCE_CHOICES
= (
362 ('expat', 'Expatrié'),
365 COMPTE_COMPTA_CHOICES
= (
371 class Dossier(DossierWorkflow
, models
.Model
):
374 employe
= models
.ForeignKey('Employe', related_name
='+', editable
=False)
375 poste
= models
.ForeignKey('Poste', related_name
='+', editable
=False)
376 statut
= models
.ForeignKey(rh
.Statut
, related_name
='+')
377 organisme_bstg
= models
.ForeignKey(rh
.OrganismeBstg
,
378 null
=True, blank
=True,
379 verbose_name
="Organisme",
380 help_text
="Si détaché (DET) ou mis à disposition (MAD), \
381 préciser l'organisme.",
383 organisme_bstg_autre
= models
.CharField(max_length
=255,
384 verbose_name
="Autre organisme",
385 help_text
="indiquer l'organisme ici s'il n'est pas dans la liste",
389 # Données antérieures de l'employé
390 statut_anterieur
= models
.ForeignKey(
391 rh
.Statut
, related_name
='+', null
=True, blank
=True,
392 verbose_name
='Statut antérieur')
393 classement_anterieur
= models
.ForeignKey(
394 rh
.Classement
, related_name
='+', null
=True, blank
=True,
395 verbose_name
='Classement précédent')
396 salaire_anterieur
= models
.DecimalField(
397 max_digits
=12, decimal_places
=2, null
=True, default
=None,
398 blank
=True, verbose_name
='Salaire précédent')
400 # Données du titulaire précédent
401 employe_anterieur
= models
.ForeignKey(
402 rh
.Employe
, related_name
='+', null
=True, blank
=True,
403 verbose_name
='Employé précédent')
404 statut_titulaire_anterieur
= models
.ForeignKey(
405 rh
.Statut
, related_name
='+', null
=True, blank
=True,
406 verbose_name
='Statut titulaire précédent')
407 classement_titulaire_anterieur
= models
.ForeignKey(
408 rh
.Classement
, related_name
='+', null
=True, blank
=True,
409 verbose_name
='Classement titulaire précédent')
410 salaire_titulaire_anterieur
= models
.DecimalField(
411 max_digits
=12, decimal_places
=2, default
=None, null
=True,
412 blank
=True, verbose_name
='Salaire titulaire précédent')
415 remplacement
= models
.BooleanField()
416 statut_residence
= models
.CharField(max_length
=10, default
='local',
417 verbose_name
="Statut",
418 choices
=STATUT_RESIDENCE_CHOICES
)
421 classement
= models
.ForeignKey(rh
.Classement
, related_name
='+',
422 null
=True, blank
=True,
423 verbose_name
='Classement proposé')
424 salaire
= models
.DecimalField(max_digits
=12, decimal_places
=2,
425 verbose_name
='Salaire de base',
426 null
=True, default
=None)
427 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+')
428 regime_travail
= models
.DecimalField(max_digits
=12,
430 default
=REGIME_TRAVAIL_DEFAULT
,
431 verbose_name
="Régime de travail",
432 help_text
="% du temps complet")
433 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
435 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
436 verbose_name
="Nb. heures par semaine")
439 type_contrat
= models
.ForeignKey(rh
.TypeContrat
, related_name
='+')
440 contrat_date_debut
= models
.DateField(help_text
="format: aaaa-mm-jj")
441 contrat_date_fin
= models
.DateField(null
=True, blank
=True,
442 help_text
="format: aaaa-mm-jj")
445 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
446 verbose_name
=u
'Compte comptabilité',
447 choices
=COMPTE_COMPTA_CHOICES
)
448 compte_courriel
= models
.BooleanField()
451 date_creation
= models
.DateTimeField(auto_now_add
=True)
454 objects
= DossierManager()
456 def __unicode__(self
):
457 return u
'[%s] %s - %s' % (self
.poste
.implantation
, self
.poste
.nom
, self
.employe
)
459 def get_salaire_euros(self
):
461 tx
= rh
.TauxChange
.objects
.filter(implantation
=self
.poste
.implantation
, devise
=self
.devise
)[0].taux
464 return (float)(tx
) * (float)(self
.salaire
)
466 def get_couts_auf(self
):
468 On retire les MAD BSTG
470 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
not in (2, )]
472 def get_total_couts_auf(self
):
474 for r
in self
.get_couts_auf():
475 total
+= r
.montant_euro()
478 def get_aides_auf(self
):
480 On récupère les MAD BSTG
482 return [r
for r
in self
.remuneration_set
.all() if r
.type_id
in (2, )]
484 def get_total_aides_auf(self
):
486 for r
in self
.get_aides_auf():
487 total
+= r
.montant_euro()
490 def get_total_remun(self
):
491 return self
.get_total_couts_auf() + self
.get_total_aides_auf()
493 # Tester l'enregistrement car les models.py sont importés au complet
494 if not reversion
.is_registered(Dossier
):
495 reversion
.register(Dossier
)
497 class DossierPiece(models
.Model
):
498 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
499 Ex.: Lettre de motivation.
501 dossier
= models
.ForeignKey("Dossier")
502 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
503 fichier
= models
.FileField(verbose_name
="Fichier",
504 upload_to
=dossier_piece_dispatch
,
505 storage
=storage_prive
)
508 class DossierComparaison(models
.Model
):
510 Photo d'une comparaison salariale au moment de l'embauche.
512 dossier
= models
.ForeignKey('Dossier', related_name
='comparaisons')
513 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True)
514 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
515 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
516 montant
= models
.IntegerField(null
=True)
517 devise
= models
.ForeignKey(rh
.Devise
, default
=5, related_name
='+', null
=True, blank
=True)
518 montant_euros
= models
.IntegerField(null
=True)
523 class Remuneration(models
.Model
):
525 dossier
= models
.ForeignKey('Dossier', db_column
='dossier')
526 type = models
.ForeignKey(rh
.TypeRemuneration
, db_column
='type',
528 montant
= models
.DecimalField(max_digits
=12, decimal_places
=2,
530 devise
= models
.ForeignKey(rh
.Devise
, to_field
='code',
531 db_column
='devise', related_name
='+')
532 precision
= models
.CharField(max_length
=255, null
=True, blank
=True)
535 date_creation
= models
.DateField(auto_now_add
=True)
536 user_creation
= models
.IntegerField(null
=True, blank
=True) # TODO : user
538 def montant_mois(self
):
539 return round(self
.montant
/ 12, 2)
541 def taux_devise(self
):
542 return self
.devise
.tauxchange_set
.order_by('-annee').all()[0].taux
544 def montant_euro(self
):
545 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
547 def montant_euro_mois(self
):
548 return round(self
.montant_euro() / 12, 2)
553 TYPE_JUSTIFICATIONS
= (
554 ('N', 'Nouvel employé'),
555 ('R', 'Renouvellement employé'),
558 class JustificationQuestion(models
.Model
):
559 question
= models
.CharField(max_length
=255)
560 type = models
.CharField(max_length
=255, choices
=TYPE_JUSTIFICATIONS
)
562 def __unicode__(self
,):
565 class JustificationNouvelEmploye(models
.Model
):
566 dossier
= models
.ForeignKey("Dossier")
567 question
= models
.ForeignKey("JustificationQuestion")
568 reponse
= models
.TextField()
570 class JustificationAutreEmploye(models
.Model
):
571 dossier
= models
.ForeignKey("Dossier")
572 question
= models
.ForeignKey("JustificationQuestion")
573 reponse
= models
.TextField()