Merge branch 'master' into dev
[auf_rh_dae.git] / project / dae / models.py
CommitLineData
bd28238f 1# -=- encoding: utf-8 -=-
3f3cf5f3 2
5633fa41 3from django.conf import settings
36341125 4from django.core.files.storage import FileSystemStorage
a9c281dd
OL
5from django.db import models
6import reversion
3f5cbabe 7from rh import models as rh
afc204bf 8from workflow import PosteWorkflow, DossierWorkflow
c511cd1f
EMS
9from workflow import DOSSIER_ETAT_DRH_FINALISATION, DOSSIER_ETAT_REGION_FINALISATION, \
10 DOSSIER_ETAT_FINALISE
a2c3ad52 11from auf.django.metadata.models import AUFMetadata
3f5cbabe 12from managers import *
4047b783 13from rh.models import HELP_TEXT_DATE
47d7067b 14from exporter import DossierCopier, PosteCopier
bd28238f 15
d766bf2c 16# Upload de fichiers
34dad720 17UPLOAD_STORAGE = FileSystemStorage(settings.PRIVE_MEDIA_ROOT)
d766bf2c 18
36341125 19
2d4d2fcf 20### POSTE
21
22POSTE_APPEL_CHOICES = (
23 ('interne', 'Interne'),
24 ('externe', 'Externe'),
25)
c3be904d
OL
26POSTE_ACTION = (
27 ('N', u"Nouveau poste"),
28 ('M', u"Poste existant"),
29 ('E', u"Évolution de poste"),
30)
31
36341125 32
ae5c920b
OL
33class DeviseException(Exception):
34 silent_variable_failure = True
35
1c7d67ce 36
3f5cbabe 37class Poste(PosteWorkflow, rh.Poste_):
c3be904d
OL
38
39 type_intervention = models.CharField(max_length=1, choices=POSTE_ACTION, default='N')
40
bd28238f 41 # Modèle existant
5d680e84 42 id_rh = models.ForeignKey(rh.Poste, null=True, related_name='+',
2d4d2fcf 43 editable=False,
c1195471 44 verbose_name=u"Mise à jour du poste")
bd28238f
NC
45
46 # Rémunération
3f5cbabe 47 indemn_expat_min = models.DecimalField(max_digits=13, decimal_places=2, default=0)
5f61bccb
OL
48 indemn_expat_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
49 indemn_fct_min = models.DecimalField(max_digits=12, decimal_places=2, default=0)
50 indemn_fct_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
51 charges_patronales_min = models.DecimalField(max_digits=12, decimal_places=2, default=0)
52 charges_patronales_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
bd28238f 53
1c7d67ce
OL
54 # Managers
55 objects = PosteManager()
56
03858ba5
OL
57 def _get_key(self):
58 """
1bc84af4 59 Les vues sont montées selon une clef spéciale
2d4d2fcf 60 pour identifier la provenance du poste.
1bc84af4 61 Cette méthode fournit un moyen de reconstruire cette clef
2d4d2fcf 62 afin de générer les URLs.
03858ba5
OL
63 """
64 return "dae-%s" % self.id
65 key = property(_get_key)
66
f3333b0e
OL
67 def get_dossiers(self):
68 """
69 Liste tous les anciens dossiers liés à ce poste.
1bc84af4 70 (Le nom de la relation sur le rh.Poste est mal choisi
2d4d2fcf 71 poste1 au lieu de dossier1)
f3333b0e 72 Note1 : seulement le dosssier principal fait l'objet de la recherche.
1bc84af4
EMS
73 Note2 : les dossiers sont retournés du plus récent au plus vieux.
74 (Ce test est fait en fonction du id,
2d4d2fcf 75 car les dates de création sont absentes de rh v1).
f3333b0e
OL
76 """
77 if self.id_rh is None:
78 return []
16b1454e 79 return self.id_rh.rh_dossiers.all()
428e3c0b 80
a47ed016
OL
81 def rh_importation(self):
82 if ImportPoste.objects.filter(dae=self).exists():
83 return ImportPoste.objects.get(dae=self).rh
84 else:
85 return None
86
87 def importer(self, verbosity=0, dry_run=False):
88 copieur = PosteCopier(verbosity=verbosity, dry_run=dry_run)
89 return copieur.copy(self)
f3333b0e
OL
90
91 def get_employe(self):
92 """
93 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
94 """
95 dossiers = self.get_dossiers()
96 if len(dossiers) > 0:
97 return dossiers[0].employe
98 else:
99 return None
100
179f6b49 101 def get_default_devise(self):
1bc84af4 102 """Récupère la devise par défaut en fonction de l'implantation
2d4d2fcf 103 (EUR autrement)
104 """
179f6b49 105 try:
6e4600ef 106 implantation_devise = rh.TauxChange.objects \
107 .filter(implantation=self.implantation)[0].devise
179f6b49
OL
108 except:
109 implantation_devise = 5 # EUR
110 return implantation_devise
111
c0413a6f
OL
112 #####################
113 # Classement de poste
114 #####################
115
116 def get_couts_minimum(self):
83b94a87
EMS
117 return self.salaire_min + self.indemn_expat_min + self.indemn_fct_min + self.charges_patronales_min + self.autre_min
118
119 def get_salaire_minimum(self):
120 return self.get_couts_minimum() - self.charges_patronales_min
c0413a6f
OL
121
122 def get_taux_minimum(self):
4e439a89
OL
123 if self.devise_min.code == 'EUR':
124 return 1
2455f48d 125 liste_taux = self.devise_min.tauxchange_set.order_by('-annee')
4e439a89
OL
126 if len(liste_taux) == 0:
127 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_min, self.implantation))
b6825282 128 else:
4e439a89 129 return liste_taux[0].taux
c0413a6f
OL
130
131 def get_couts_minimum_euros(self):
fa5b95ed 132 return float(self.get_couts_minimum()) * self.get_taux_minimum()
c0413a6f 133
83b94a87 134 def get_salaire_minimum_euros(self):
fa5b95ed 135 return float(self.get_salaire_minimum()) * self.get_taux_minimum()
83b94a87 136
c0413a6f 137 def get_couts_maximum(self):
83b94a87
EMS
138 return self.salaire_max + self.indemn_expat_max + self.indemn_fct_max + self.charges_patronales_max + self.autre_max
139
140 def get_salaire_maximum(self):
141 return self.get_couts_maximum() - self.charges_patronales_max
c0413a6f
OL
142
143 def get_taux_maximum(self):
4e439a89
OL
144 if self.devise_max.code == 'EUR':
145 return 1
2455f48d 146 liste_taux = self.devise_max.tauxchange_set.order_by('-annee')
4e439a89
OL
147 if len(liste_taux) == 0:
148 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_max, self.implantation))
b6825282 149 else:
4e439a89 150 return liste_taux[0].taux
c0413a6f
OL
151
152 def get_couts_maximum_euros(self):
fa5b95ed 153 return float(self.get_couts_maximum()) * self.get_taux_maximum()
c0413a6f 154
83b94a87 155 def get_salaire_maximum_euros(self):
fa5b95ed 156 return float(self.get_salaire_maximum()) * self.get_taux_maximum()
12c7f8a7
OL
157
158 def show_taux_minimum(self):
159 try:
160 return self.get_taux_minimum()
83b94a87 161 except DeviseException, e:
12c7f8a7
OL
162 return e
163
164 def show_couts_minimum_euros(self):
165 try:
166 return self.get_couts_minimum_euros()
83b94a87
EMS
167 except DeviseException, e:
168 return e
169
170 def show_salaire_minimum_euros(self):
171 try:
172 return self.get_salaire_minimum_euros()
173 except DeviseException, e:
12c7f8a7
OL
174 return e
175
176 def show_taux_maximum(self):
177 try:
178 return self.get_taux_maximum()
83b94a87 179 except DeviseException, e:
12c7f8a7
OL
180 return e
181
182 def show_couts_maximum_euros(self):
183 try:
184 return self.get_couts_maximum_euros()
83b94a87
EMS
185 except DeviseException, e:
186 return e
187
188 def show_salaire_maximum_euros(self):
189 try:
190 return self.get_salaire_maximum_euros()
191 except DeviseException, e:
12c7f8a7
OL
192 return e
193
194
c0413a6f
OL
195 ######################
196 # Comparaison de poste
197 ######################
a3fee9c5
OL
198
199 def est_comparable(self):
200 """
201 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
202 est comparable.
203 """
204 if self.comp_universite_min is None and \
205 self.comp_fonctionpub_min is None and \
206 self.comp_locale_min is None and \
207 self.comp_ong_min is None and \
208 self.comp_autre_min is None and \
209 self.comp_universite_max is None and \
210 self.comp_fonctionpub_max is None and \
211 self.comp_locale_max is None and \
212 self.comp_ong_max is None and \
213 self.comp_autre_max is None:
214 return False
215 else:
216 return True
1bc84af4 217
a3fee9c5 218
c0413a6f
OL
219 def get_taux_comparaison(self):
220 try:
2455f48d 221 return rh.TauxChange.objects.filter(devise=self.devise_comparaison)[0].taux
c0413a6f
OL
222 except:
223 return 1
224
225 def get_comp_universite_min_euros(self):
226 return (float)(self.comp_universite_min) * self.get_taux_comparaison()
227
228 def get_comp_fonctionpub_min_euros(self):
229 return (float)(self.comp_fonctionpub_min) * self.get_taux_comparaison()
230
231 def get_comp_locale_min_euros(self):
232 return (float)(self.comp_locale_min) * self.get_taux_comparaison()
233
234 def get_comp_ong_min_euros(self):
235 return (float)(self.comp_ong_min) * self.get_taux_comparaison()
236
237 def get_comp_autre_min_euros(self):
238 return (float)(self.comp_autre_min) * self.get_taux_comparaison()
239
240 def get_comp_universite_max_euros(self):
241 return (float)(self.comp_universite_max) * self.get_taux_comparaison()
242
243 def get_comp_fonctionpub_max_euros(self):
244 return (float)(self.comp_fonctionpub_max) * self.get_taux_comparaison()
245
246 def get_comp_locale_max_euros(self):
247 return (float)(self.comp_locale_max) * self.get_taux_comparaison()
248
249 def get_comp_ong_max_euros(self):
250 return (float)(self.comp_ong_max) * self.get_taux_comparaison()
251
252 def get_comp_autre_max_euros(self):
253 return (float)(self.comp_autre_max) * self.get_taux_comparaison()
254
a9e52624 255
5d680e84 256 def __unicode__(self):
f3333b0e 257 """
1bc84af4 258 Cette fonction est consommatrice SQL car elle cherche les dossiers
2d4d2fcf 259 qui ont été liés à celui-ci.
f3333b0e 260 """
f3333b0e
OL
261 data = (
262 self.implantation,
263 self.type_poste.nom,
264 self.nom,
f3333b0e 265 )
a7c68130 266 return u'%s - %s (%s)' % data
5d680e84 267
bd28238f 268
a9c281dd
OL
269# Tester l'enregistrement car les models.py sont importés au complet
270if not reversion.is_registered(Poste):
271 reversion.register(Poste)
272
bd28238f 273
5d680e84
NC
274POSTE_FINANCEMENT_CHOICES = (
275 ('A', 'A - Frais de personnel'),
276 ('B', 'B - Projet(s)-Titre(s)'),
277 ('C', 'C - Autre')
278)
bd28238f 279
317ce433 280class PosteFinancement(rh.PosteFinancement_):
a4125771 281 pass
1d0f4eef 282
317ce433 283class PostePiece(rh.PostePiece_):
a4125771 284 pass
068d1462 285
317ce433 286class PosteComparaison(rh.PosteComparaison_):
766ca378
OL
287 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name=u'Statut', null=True, blank=True, )
288 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name=u'Classement', null=True, blank=True, )
068d1462 289
2d4d2fcf 290### EMPLOYÉ/PERSONNE
291
292# TODO : migration pour m -> M, f -> F
c589d980 293
bd28238f 294GENRE_CHOICES = (
139686f2
NC
295 ('m', 'Homme'),
296 ('f', 'Femme'),
bd28238f
NC
297)
298
a2c3ad52 299class Employe(AUFMetadata):
bd28238f
NC
300
301 # Modèle existant
428e3c0b 302 id_rh = models.ForeignKey(rh.Employe, null=True, related_name='+',
c1195471 303 verbose_name=u'Employé')
bd28238f 304 nom = models.CharField(max_length=255)
c1195471 305 prenom = models.CharField(max_length=255, verbose_name=u'Prénom')
07b40eda 306 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
bd28238f 307
139686f2 308 def __unicode__(self):
2d4d2fcf 309 return u'%s %s' % (self.prenom, self.nom.upper())
139686f2 310
bd28238f 311
2d4d2fcf 312### DOSSIER
313
314STATUT_RESIDENCE_CHOICES = (
315 ('local', 'Local'),
316 ('expat', 'Expatrié'),
317)
318
bd28238f 319COMPTE_COMPTA_CHOICES = (
494ff2be
NC
320 ('coda', 'CODA'),
321 ('scs', 'SCS'),
322 ('aucun', 'Aucun'),
bd28238f
NC
323)
324
16b1454e
OL
325class Dossier(DossierWorkflow, rh.Dossier_):
326 poste = models.ForeignKey('Poste', db_column='poste', related_name='%(app_label)s_dossiers')
327 employe = models.ForeignKey('Employe', db_column='employe',
328 related_name='%(app_label)s_dossiers',
329 verbose_name=u"Employé")
0288adb5 330 organisme_bstg_autre = models.CharField(max_length=255,
c1195471 331 verbose_name=u"Autre organisme",
0288adb5
OL
332 help_text="indiquer l'organisme ici s'il n'est pas dans la liste",
333 null=True,
334 blank=True,)
bd28238f 335
139686f2
NC
336 # Données antérieures de l'employé
337 statut_anterieur = models.ForeignKey(
338 rh.Statut, related_name='+', null=True, blank=True,
c1195471 339 verbose_name=u'Statut antérieur')
139686f2
NC
340 classement_anterieur = models.ForeignKey(
341 rh.Classement, related_name='+', null=True, blank=True,
c1195471 342 verbose_name=u'Classement précédent')
139686f2
NC
343 salaire_anterieur = models.DecimalField(
344 max_digits=12, decimal_places=2, null=True, default=None,
481fbd33 345 blank=True, verbose_name=u'Salaire précédent')
e158c6de
DB
346 devise_anterieur = models.ForeignKey(rh.Devise, related_name='+',
347 null=True, blank=True)
348 type_contrat_anterieur = models.ForeignKey(rh.TypeContrat,
349 related_name='+', null=True, blank=True,
350 verbose_name=u'Type contrat antérieur', )
139686f2
NC
351
352 # Données du titulaire précédent
353 employe_anterieur = models.ForeignKey(
354 rh.Employe, related_name='+', null=True, blank=True,
c1195471 355 verbose_name=u'Employé précédent')
139686f2
NC
356 statut_titulaire_anterieur = models.ForeignKey(
357 rh.Statut, related_name='+', null=True, blank=True,
c1195471 358 verbose_name=u'Statut titulaire précédent')
139686f2
NC
359 classement_titulaire_anterieur = models.ForeignKey(
360 rh.Classement, related_name='+', null=True, blank=True,
c1195471 361 verbose_name=u'Classement titulaire précédent')
139686f2
NC
362 salaire_titulaire_anterieur = models.DecimalField(
363 max_digits=12, decimal_places=2, default=None, null=True,
481fbd33 364 blank=True, verbose_name=u'Salaire titulaire précédent')
f8c7c0b8 365 devise_titulaire_anterieur = models.ForeignKey(rh.Devise, related_name='+', null=True, blank=True)
494ff2be 366
bd28238f 367 # Rémunération
16b1454e 368 salaire = models.DecimalField(max_digits=13, decimal_places=2,
c1195471 369 verbose_name=u'Salaire de base',
2d4d2fcf 370 null=True, default=None)
e8e75458 371 devise = models.ForeignKey(rh.Devise, default=5, related_name='+')
bd28238f
NC
372
373 # Contrat
5d680e84 374 type_contrat = models.ForeignKey(rh.TypeContrat, related_name='+')
b666864e 375 contrat_date_debut = models.DateField(help_text=HELP_TEXT_DATE)
1f109689 376 contrat_date_fin = models.DateField(null=True, blank=True,
b666864e 377 help_text=HELP_TEXT_DATE)
bd28238f 378
29dffede
OL
379 # Justifications
380 justif_nouveau_statut_label = u'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
381 justif_nouveau_statut = models.TextField(verbose_name=justif_nouveau_statut_label, null=True, blank=True)
a83daab3 382 justif_nouveau_tmp_remplacement_label = u"Si l'employé effectue un remplacement temporaire, préciser"
29dffede 383 justif_nouveau_tmp_remplacement = models.TextField(verbose_name=justif_nouveau_tmp_remplacement_label, null=True, blank=True)
a83daab3 384 justif_nouveau_salaire_label = u"Si le salaire de l'employé ne correspond pas au classement du poste ou est différent du salaire antérieur, justifier "
29dffede 385 justif_nouveau_salaire = models.TextField(verbose_name=justif_nouveau_salaire_label, null=True, blank=True)
a83daab3 386 justif_nouveau_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
29dffede
OL
387 justif_nouveau_commentaire = models.TextField(verbose_name=justif_nouveau_commentaire_label, null=True, blank=True)
388 justif_rempl_type_contrat_label = u"Changement de type de contrat, ex : d'un CDD en CDI"
389 justif_rempl_type_contrat = models.TextField(verbose_name=justif_rempl_type_contrat_label, null=True, blank=True)
a83daab3 390 justif_rempl_statut_employe_label = u"Si le statut de l'employé a été modifié pour ce poste ; ex :national, expatrié, màd, détachement ? Si oui, justifier"
29dffede 391 justif_rempl_statut_employe = models.TextField(verbose_name=justif_rempl_statut_employe_label, null=True, blank=True)
a83daab3 392 justif_rempl_evaluation_label = u"L'évaluation de l'employé est-elle favorable? Préciser"
29dffede
OL
393 justif_rempl_evaluation = models.TextField(verbose_name=justif_rempl_evaluation_label, null=True, blank=True)
394 justif_rempl_salaire_label = u"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
395 justif_rempl_salaire = models.TextField(verbose_name=justif_rempl_salaire_label, null=True, blank=True)
a83daab3 396 justif_rempl_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
29dffede
OL
397 justif_rempl_commentaire = models.TextField(verbose_name=justif_rempl_commentaire_label, null=True, blank=True)
398
bd28238f 399 # Comptes
dfc2c344 400 compte_compta = models.CharField(max_length=10, default='aucun',
401 verbose_name=u'Compte comptabilité',
402 choices=COMPTE_COMPTA_CHOICES)
bd28238f 403 compte_courriel = models.BooleanField()
428e3c0b 404
c3f0b49f
EMS
405 # DAE numérisée
406 dae_numerisee = models.FileField(upload_to='dae/dae_numerisee', storage=UPLOAD_STORAGE,
5be87e56 407 blank=True, null=True, verbose_name="DAE numérisée")
c3f0b49f 408
e4f56614
OL
409 # Managers
410 objects = DossierManager()
428e3c0b 411
16b1454e
OL
412 def __init__(self, *args, **kwargs):
413 # Bouchon pour créer une date fictive necessaire pour valider un dossier
414 # à cause de l'héritage
415 super(rh.Dossier_, self).__init__(*args, **kwargs)
416 super(DossierWorkflow, self).__init__(*args, **kwargs)
417 import datetime
418 self.date_debut = datetime.datetime.today()
419
aec2c91e 420 def __unicode__(self):
e4f56614 421 return u'[%s] %s - %s' % (self.poste.implantation, self.poste.nom, self.employe)
bd28238f 422
47d7067b
OL
423 def importer(self, verbosity=0, dry_run=False):
424 copieur = DossierCopier(verbosity=verbosity, dry_run=dry_run)
59c67b87 425 return copieur.copy(self)
317ce433 426
eed93931 427 def get_salaire_anterieur_euros(self):
88a0e98d
OL
428 if self.devise_anterieur is None:
429 return None
03ff41e3
OL
430 try:
431 taux = self.taux_devise(self.devise_anterieur)
432 except Exception, e:
433 return e
434 if not taux:
435 return None
436 return int(round(float(self.salaire_anterieur) * float(taux), 2))
437
eed93931
OL
438
439 def get_salaire_titulaire_anterieur_euros(self):
16b1454e
OL
440 if self.devise_titulaire_anterieur is None:
441 return None
03ff41e3 442 try:
88a0e98d 443 taux = self.taux_devise(self.devise_titulaire_anterieur)
03ff41e3
OL
444 except Exception, e:
445 return e
446 if not taux:
16b1454e 447 return None
03ff41e3 448 return int(round(float(self.salaire_titulaire_anterieur) * float(taux), 2))
eed93931 449
c511cd1f
EMS
450 def valide(self):
451 return self.etat in (DOSSIER_ETAT_REGION_FINALISATION,
452 DOSSIER_ETAT_DRH_FINALISATION,
453 DOSSIER_ETAT_FINALISE)
454
bd28238f 455
0140cbd2 456# Tester l'enregistrement car les models.py sont importés au complet
457if not reversion.is_registered(Dossier):
458 reversion.register(Dossier)
bd28238f 459
a4125771 460class DossierPiece(rh.DossierPiece_):
2d4d2fcf 461 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
462 Ex.: Lettre de motivation.
463 """
a4125771 464 pass
2d4d2fcf 465
a4125771 466class DossierComparaison(rh.DossierComparaison_):
03b395db
OL
467 """
468 Photo d'une comparaison salariale au moment de l'embauche.
469 """
766ca378
OL
470 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name='Statut', null=True, blank=True, )
471 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name='Classement', null=True, blank=True, )
03b395db 472
c589d980 473
2d4d2fcf 474### RÉMUNÉRATION
475
a4125771
OL
476class Remuneration(rh.Remuneration_):
477 pass
c1834169
EMS
478
479### CONTRATS
480
a4125771
OL
481class Contrat(rh.Contrat_):
482 pass
317ce433 483
47d7067b
OL
484
485class DossierFinalise(Dossier):
486
487 objects = DossierFinaliseManager()
488
489 class Meta:
490 proxy = True
86ffe634
OL
491 verbose_name = "Import d'un dossier dans RH"
492 verbose_name_plural = "Import des dossiers dans RH"
47d7067b 493
a47ed016
OL
494class PosteFinalise(Poste):
495
496 objects = PosteFinaliseManager()
497
498 class Meta:
499 proxy = True
86ffe634
OL
500 verbose_name = "Import d'un poste dans RH"
501 verbose_name_plural = "Import des postes dans RH"
a47ed016 502
317ce433
OL
503# modèle de liaison entre les systèmes
504
505class ImportDossier(models.Model):
506 dae = models.ForeignKey('dae.Dossier', related_name='+')
507 rh = models.ForeignKey('rh.Dossier', related_name='+')
508
509class ImportPoste(models.Model):
510 dae = models.ForeignKey('dae.Poste', related_name='+')
511 rh = models.ForeignKey('rh.Poste', related_name='+')