cleanup
[auf_rh_dae.git] / project / dae / models.py
1 # -=- encoding: utf-8 -=-
2
3 from django.conf import settings
4 from django.core.files.storage import FileSystemStorage
5 from django.db import models
6 import reversion
7 from rh import models as rh
8 from workflow import PosteWorkflow, DossierWorkflow
9 from workflow import DOSSIER_ETAT_DRH_FINALISATION, DOSSIER_ETAT_REGION_FINALISATION, \
10 DOSSIER_ETAT_FINALISE
11 from auf.django.metadata.models import AUFMetadata
12 from managers import *
13 from rh.models import HELP_TEXT_DATE
14 from exporter import DossierCopier, PosteCopier
15
16 # Upload de fichiers
17 UPLOAD_STORAGE = FileSystemStorage(settings.PRIVE_MEDIA_ROOT)
18
19
20 ### POSTE
21
22 POSTE_APPEL_CHOICES = (
23 ('interne', 'Interne'),
24 ('externe', 'Externe'),
25 )
26 POSTE_ACTION = (
27 ('N', u"Nouveau poste"),
28 ('M', u"Poste existant"),
29 ('E', u"Évolution de poste"),
30 )
31
32
33 class DeviseException(Exception):
34 silent_variable_failure = True
35
36
37 class Poste(PosteWorkflow, rh.Poste_):
38
39 type_intervention = models.CharField(max_length=1, choices=POSTE_ACTION, default='N')
40
41 # Modèle existant
42 id_rh = models.ForeignKey(rh.Poste, null=True, related_name='+',
43 editable=False,
44 verbose_name=u"Mise à jour du poste")
45
46 # Rémunération
47 indemn_expat_min = models.DecimalField(max_digits=13, decimal_places=2, default=0)
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)
53
54 # Managers
55 objects = PosteManager()
56
57 def _get_key(self):
58 """
59 Les vues sont montées selon une clef spéciale
60 pour identifier la provenance du poste.
61 Cette méthode fournit un moyen de reconstruire cette clef
62 afin de générer les URLs.
63 """
64 return "dae-%s" % self.id
65 key = property(_get_key)
66
67 def get_dossiers(self):
68 """
69 Liste tous les anciens dossiers liés à ce poste.
70 (Le nom de la relation sur le rh.Poste est mal choisi
71 poste1 au lieu de dossier1)
72 Note1 : seulement le dosssier principal fait l'objet de la recherche.
73 Note2 : les dossiers sont retournés du plus récent au plus vieux.
74 (Ce test est fait en fonction du id,
75 car les dates de création sont absentes de rh v1).
76 """
77 if self.id_rh is None:
78 return []
79 return self.id_rh.rh_dossiers.all()
80
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)
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
101 def get_default_devise(self):
102 """Récupère la devise par défaut en fonction de l'implantation
103 (EUR autrement)
104 """
105 try:
106 implantation_devise = rh.TauxChange.objects \
107 .filter(implantation=self.implantation)[0].devise
108 except:
109 implantation_devise = 5 # EUR
110 return implantation_devise
111
112 #####################
113 # Classement de poste
114 #####################
115
116 def get_couts_minimum(self):
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
121
122 def get_taux_minimum(self):
123 if self.devise_min.code == 'EUR':
124 return 1
125 liste_taux = self.devise_min.tauxchange_set.order_by('-annee')
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))
128 else:
129 return liste_taux[0].taux
130
131 def get_couts_minimum_euros(self):
132 return float(self.get_couts_minimum()) * self.get_taux_minimum()
133
134 def get_salaire_minimum_euros(self):
135 return float(self.get_salaire_minimum()) * self.get_taux_minimum()
136
137 def get_couts_maximum(self):
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
142
143 def get_taux_maximum(self):
144 if self.devise_max.code == 'EUR':
145 return 1
146 liste_taux = self.devise_max.tauxchange_set.order_by('-annee')
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))
149 else:
150 return liste_taux[0].taux
151
152 def get_couts_maximum_euros(self):
153 return float(self.get_couts_maximum()) * self.get_taux_maximum()
154
155 def get_salaire_maximum_euros(self):
156 return float(self.get_salaire_maximum()) * self.get_taux_maximum()
157
158 def show_taux_minimum(self):
159 try:
160 return self.get_taux_minimum()
161 except DeviseException, e:
162 return e
163
164 def show_couts_minimum_euros(self):
165 try:
166 return self.get_couts_minimum_euros()
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:
174 return e
175
176 def show_taux_maximum(self):
177 try:
178 return self.get_taux_maximum()
179 except DeviseException, e:
180 return e
181
182 def show_couts_maximum_euros(self):
183 try:
184 return self.get_couts_maximum_euros()
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:
192 return e
193
194
195 ######################
196 # Comparaison de poste
197 ######################
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
217
218
219 def get_taux_comparaison(self):
220 try:
221 return rh.TauxChange.objects.filter(devise=self.devise_comparaison)[0].taux
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
255
256 def __unicode__(self):
257 """
258 Cette fonction est consommatrice SQL car elle cherche les dossiers
259 qui ont été liés à celui-ci.
260 """
261 data = (
262 self.implantation,
263 self.type_poste.nom,
264 self.nom,
265 )
266 return u'%s - %s (%s)' % data
267
268
269 # Tester l'enregistrement car les models.py sont importés au complet
270 if not reversion.is_registered(Poste):
271 reversion.register(Poste)
272
273
274 POSTE_FINANCEMENT_CHOICES = (
275 ('A', 'A - Frais de personnel'),
276 ('B', 'B - Projet(s)-Titre(s)'),
277 ('C', 'C - Autre')
278 )
279
280 class PosteFinancement(rh.PosteFinancement_):
281 pass
282
283 class PostePiece(rh.PostePiece_):
284 pass
285
286 class PosteComparaison(rh.PosteComparaison_):
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, )
289
290 ### EMPLOYÉ/PERSONNE
291
292 # TODO : migration pour m -> M, f -> F
293
294 GENRE_CHOICES = (
295 ('m', 'Homme'),
296 ('f', 'Femme'),
297 )
298
299 class Employe(AUFMetadata):
300
301 # Modèle existant
302 id_rh = models.ForeignKey(rh.Employe, null=True, related_name='+',
303 verbose_name=u'Employé')
304 nom = models.CharField(max_length=255)
305 prenom = models.CharField(max_length=255, verbose_name=u'Prénom')
306 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
307
308 def __unicode__(self):
309 return u'%s %s' % (self.prenom, self.nom.upper())
310
311
312 ### DOSSIER
313
314 STATUT_RESIDENCE_CHOICES = (
315 ('local', 'Local'),
316 ('expat', 'Expatrié'),
317 )
318
319 COMPTE_COMPTA_CHOICES = (
320 ('coda', 'CODA'),
321 ('scs', 'SCS'),
322 ('aucun', 'Aucun'),
323 )
324
325 class 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é")
330 organisme_bstg_autre = models.CharField(max_length=255,
331 verbose_name=u"Autre organisme",
332 help_text="indiquer l'organisme ici s'il n'est pas dans la liste",
333 null=True,
334 blank=True,)
335
336 # Données antérieures de l'employé
337 statut_anterieur = models.ForeignKey(
338 rh.Statut, related_name='+', null=True, blank=True,
339 verbose_name=u'Statut antérieur')
340 classement_anterieur = models.ForeignKey(
341 rh.Classement, related_name='+', null=True, blank=True,
342 verbose_name=u'Classement précédent')
343 salaire_anterieur = models.DecimalField(
344 max_digits=12, decimal_places=2, null=True, default=None,
345 blank=True, verbose_name=u'Salaire précédent')
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', )
351
352 # Données du titulaire précédent
353 employe_anterieur = models.ForeignKey(
354 rh.Employe, related_name='+', null=True, blank=True,
355 verbose_name=u'Employé précédent')
356 statut_titulaire_anterieur = models.ForeignKey(
357 rh.Statut, related_name='+', null=True, blank=True,
358 verbose_name=u'Statut titulaire précédent')
359 classement_titulaire_anterieur = models.ForeignKey(
360 rh.Classement, related_name='+', null=True, blank=True,
361 verbose_name=u'Classement titulaire précédent')
362 salaire_titulaire_anterieur = models.DecimalField(
363 max_digits=12, decimal_places=2, default=None, null=True,
364 blank=True, verbose_name=u'Salaire titulaire précédent')
365 devise_titulaire_anterieur = models.ForeignKey(rh.Devise, related_name='+', null=True, blank=True)
366
367 # Rémunération
368 salaire = models.DecimalField(max_digits=13, decimal_places=2,
369 verbose_name=u'Salaire de base',
370 null=True, default=None)
371 devise = models.ForeignKey(rh.Devise, default=5, related_name='+')
372
373 # Contrat
374 type_contrat = models.ForeignKey(rh.TypeContrat, related_name='+')
375 contrat_date_debut = models.DateField(help_text=HELP_TEXT_DATE)
376 contrat_date_fin = models.DateField(null=True, blank=True,
377 help_text=HELP_TEXT_DATE)
378
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)
382 justif_nouveau_tmp_remplacement_label = u"Si l'employé effectue un remplacement temporaire, préciser"
383 justif_nouveau_tmp_remplacement = models.TextField(verbose_name=justif_nouveau_tmp_remplacement_label, null=True, blank=True)
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 "
385 justif_nouveau_salaire = models.TextField(verbose_name=justif_nouveau_salaire_label, null=True, blank=True)
386 justif_nouveau_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
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)
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"
391 justif_rempl_statut_employe = models.TextField(verbose_name=justif_rempl_statut_employe_label, null=True, blank=True)
392 justif_rempl_evaluation_label = u"L'évaluation de l'employé est-elle favorable? Préciser"
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)
396 justif_rempl_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
397 justif_rempl_commentaire = models.TextField(verbose_name=justif_rempl_commentaire_label, null=True, blank=True)
398
399 # Comptes
400 compte_compta = models.CharField(max_length=10, default='aucun',
401 verbose_name=u'Compte comptabilité',
402 choices=COMPTE_COMPTA_CHOICES)
403 compte_courriel = models.BooleanField()
404
405 # DAE numérisée
406 dae_numerisee = models.FileField(upload_to='dae/dae_numerisee', storage=UPLOAD_STORAGE,
407 blank=True, null=True, verbose_name="DAE numérisée")
408
409 # Managers
410 objects = DossierManager()
411
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
420 def __unicode__(self):
421 return u'[%s] %s - %s' % (self.poste.implantation, self.poste.nom, self.employe)
422
423 def importer(self, verbosity=0, dry_run=False):
424 copieur = DossierCopier(verbosity=verbosity, dry_run=dry_run)
425 return copieur.copy(self)
426
427 def get_salaire_anterieur_euros(self):
428 if self.devise_anterieur is None:
429 return None
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
438
439 def get_salaire_titulaire_anterieur_euros(self):
440 if self.devise_titulaire_anterieur is None:
441 return None
442 try:
443 taux = self.taux_devise(self.devise_titulaire_anterieur)
444 except Exception, e:
445 return e
446 if not taux:
447 return None
448 return int(round(float(self.salaire_titulaire_anterieur) * float(taux), 2))
449
450 def valide(self):
451 return self.etat in (DOSSIER_ETAT_REGION_FINALISATION,
452 DOSSIER_ETAT_DRH_FINALISATION,
453 DOSSIER_ETAT_FINALISE)
454
455
456 # Tester l'enregistrement car les models.py sont importés au complet
457 if not reversion.is_registered(Dossier):
458 reversion.register(Dossier)
459
460 class DossierPiece(rh.DossierPiece_):
461 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
462 Ex.: Lettre de motivation.
463 """
464 pass
465
466 class DossierComparaison(rh.DossierComparaison_):
467 """
468 Photo d'une comparaison salariale au moment de l'embauche.
469 """
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, )
472
473
474 ### RÉMUNÉRATION
475
476 class Remuneration(rh.Remuneration_):
477 pass
478
479 ### CONTRATS
480
481 class Contrat(rh.Contrat_):
482 pass
483
484
485 class DossierFinalise(Dossier):
486
487 objects = DossierFinaliseManager()
488
489 class Meta:
490 proxy = True
491 verbose_name = "Import d'un dossier dans RH"
492 verbose_name_plural = "Import des dossiers dans RH"
493
494 class PosteFinalise(Poste):
495
496 objects = PosteFinaliseManager()
497
498 class Meta:
499 proxy = True
500 verbose_name = "Import d'un poste dans RH"
501 verbose_name_plural = "Import des postes dans RH"
502
503 # modèle de liaison entre les systèmes
504
505 class ImportDossier(models.Model):
506 dae = models.ForeignKey('dae.Dossier', related_name='+')
507 rh = models.ForeignKey('rh.Dossier', related_name='+')
508
509 class ImportPoste(models.Model):
510 dae = models.ForeignKey('dae.Poste', related_name='+')
511 rh = models.ForeignKey('rh.Poste', related_name='+')