poste dae -> poste rh
[auf_rh_dae.git] / project / dae / models.py
1 # -=- encoding: utf-8 -=-
2
3 import os
4 from django.conf import settings
5 from django.core.files.storage import FileSystemStorage
6 from django.db import models
7 import reversion
8 from rh import models as rh
9 from workflow import PosteWorkflow, DossierWorkflow
10 from workflow import DOSSIER_ETAT_DRH_FINALISATION, DOSSIER_ETAT_REGION_FINALISATION, \
11 DOSSIER_ETAT_FINALISE
12 import auf.django.references.models as ref
13 from managers import *
14
15
16 # Constantes
17 HELP_TEXT_DATE = "format: jj-mm-aaaa"
18 REGIME_TRAVAIL_DEFAULT=100.00
19 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT=35.00
20
21 # Upload de fichiers
22 UPLOAD_STORAGE = FileSystemStorage(settings.PRIVE_MEDIA_ROOT)
23
24
25 ### POSTE
26
27 POSTE_APPEL_CHOICES = (
28 ('interne', 'Interne'),
29 ('externe', 'Externe'),
30 )
31 POSTE_ACTION = (
32 ('N', u"Nouveau poste"),
33 ('M', u"Poste existant"),
34 ('E', u"Évolution de poste"),
35 )
36
37
38 class DeviseException(Exception):
39 silent_variable_failure = True
40
41
42 class Poste(PosteWorkflow, rh.Poste_):
43
44 type_intervention = models.CharField(max_length=1, choices=POSTE_ACTION, default='N')
45
46 # Modèle existant
47 id_rh = models.ForeignKey(rh.Poste, null=True, related_name='+',
48 editable=False,
49 verbose_name=u"Mise à jour du poste")
50
51 # Rémunération
52 indemn_expat_min = models.DecimalField(max_digits=13, decimal_places=2, default=0)
53 indemn_expat_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
54 indemn_fct_min = models.DecimalField(max_digits=12, decimal_places=2, default=0)
55 indemn_fct_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
56 charges_patronales_min = models.DecimalField(max_digits=12, decimal_places=2, default=0)
57 charges_patronales_max = models.DecimalField(max_digits=12, decimal_places=2, default=0)
58
59 # Managers
60 objects = PosteManager()
61
62
63 def est_importe(self):
64 """Test si le poste a déjà été importé"""
65 return ImportPoste.objects.filter(dae=self).exists()
66
67 def importer(self):
68 from django.db.models import AutoField
69 if self.est_importe():
70 return ImportPoste.objects.get(dae=self)
71 rh_poste = rh.Poste()
72 # Faire une copie profonde de l'objet.
73 # PosteFinancement, PosteComparaison, Remun modele a ajuster...
74
75 def copy_model(src, dst, exclude=[]):
76 keys = [f.name for f in src._meta.fields if f.name not in ['id', ] + exclude]
77 for k in keys:
78 setattr(dst, k, getattr(src, k))
79 return dst
80
81 rh_poste = copy_model(self, rh_poste)
82 rh_poste.save()
83 print rh_poste.id
84
85 for o in self.dae_financements.all():
86 rh_financement = rh.PosteFinancement()
87 rh_financement = copy_model(o, rh_financement, exclude=['poste',])
88 rh_financement.poste = rh_poste
89 rh_financement.save()
90
91 for o in self.dae_pieces.all():
92 rh_piece = rh.PostePiece()
93 rh_piece = copy_model(o, rh_piece, exclude=['poste',])
94 rh_piece.poste = rh_poste
95 rh_piece.save()
96
97 for o in self.dae_comparaisons_internes.all():
98 rh_comp = rh.PosteComparaison()
99 rh_comp = copy_model(o, rh_financement, exclude=['poste',])
100 rh_comp.poste = rh_poste
101 rh_comp.save()
102
103 return rh_poste
104
105 def _get_key(self):
106 """
107 Les vues sont montées selon une clef spéciale
108 pour identifier la provenance du poste.
109 Cette méthode fournit un moyen de reconstruire cette clef
110 afin de générer les URLs.
111 """
112 return "dae-%s" % self.id
113 key = property(_get_key)
114
115 def get_dossiers(self):
116 """
117 Liste tous les anciens dossiers liés à ce poste.
118 (Le nom de la relation sur le rh.Poste est mal choisi
119 poste1 au lieu de dossier1)
120 Note1 : seulement le dosssier principal fait l'objet de la recherche.
121 Note2 : les dossiers sont retournés du plus récent au plus vieux.
122 (Ce test est fait en fonction du id,
123 car les dates de création sont absentes de rh v1).
124 """
125 if self.id_rh is None:
126 return []
127 return self.id_rh.rh_dossiers.all()
128
129 def get_complement_nom(self):
130 """
131 Inspecte les modèles rh v1 pour trouver dans le dernier dossier
132 un complément de titre de poste.
133 """
134 dossiers = self.get_dossiers()
135 if len(dossiers) > 0:
136 nom = dossiers[0].poste.nom
137 else:
138 nom = ""
139 return nom
140
141 def get_employe(self):
142 """
143 Inspecte les modèles rh v1 pour trouver l'employé du dernier dossier.
144 """
145 dossiers = self.get_dossiers()
146 if len(dossiers) > 0:
147 return dossiers[0].employe
148 else:
149 return None
150
151 def get_default_devise(self):
152 """Récupère la devise par défaut en fonction de l'implantation
153 (EUR autrement)
154 """
155 try:
156 implantation_devise = rh.TauxChange.objects \
157 .filter(implantation=self.implantation)[0].devise
158 except:
159 implantation_devise = 5 # EUR
160 return implantation_devise
161
162 #####################
163 # Classement de poste
164 #####################
165
166 def get_couts_minimum(self):
167 return self.salaire_min + self.indemn_expat_min + self.indemn_fct_min + self.charges_patronales_min + self.autre_min
168
169 def get_salaire_minimum(self):
170 return self.get_couts_minimum() - self.charges_patronales_min
171
172 def get_taux_minimum(self):
173 if self.devise_min.code == 'EUR':
174 return 1
175 liste_taux = self.devise_min.tauxchange_set.order_by('-annee')
176 if len(liste_taux) == 0:
177 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_min, self.implantation))
178 else:
179 return liste_taux[0].taux
180
181 def get_couts_minimum_euros(self):
182 return float(self.get_couts_minimum()) * self.get_taux_minimum()
183
184 def get_salaire_minimum_euros(self):
185 return float(self.get_salaire_minimum()) * self.get_taux_minimum()
186
187 def get_couts_maximum(self):
188 return self.salaire_max + self.indemn_expat_max + self.indemn_fct_max + self.charges_patronales_max + self.autre_max
189
190 def get_salaire_maximum(self):
191 return self.get_couts_maximum() - self.charges_patronales_max
192
193 def get_taux_maximum(self):
194 if self.devise_max.code == 'EUR':
195 return 1
196 liste_taux = self.devise_max.tauxchange_set.order_by('-annee')
197 if len(liste_taux) == 0:
198 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_max, self.implantation))
199 else:
200 return liste_taux[0].taux
201
202 def get_couts_maximum_euros(self):
203 return float(self.get_couts_maximum()) * self.get_taux_maximum()
204
205 def get_salaire_maximum_euros(self):
206 return float(self.get_salaire_maximum()) * self.get_taux_maximum()
207
208 def show_taux_minimum(self):
209 try:
210 return self.get_taux_minimum()
211 except DeviseException, e:
212 return e
213
214 def show_couts_minimum_euros(self):
215 try:
216 return self.get_couts_minimum_euros()
217 except DeviseException, e:
218 return e
219
220 def show_salaire_minimum_euros(self):
221 try:
222 return self.get_salaire_minimum_euros()
223 except DeviseException, e:
224 return e
225
226 def show_taux_maximum(self):
227 try:
228 return self.get_taux_maximum()
229 except DeviseException, e:
230 return e
231
232 def show_couts_maximum_euros(self):
233 try:
234 return self.get_couts_maximum_euros()
235 except DeviseException, e:
236 return e
237
238 def show_salaire_maximum_euros(self):
239 try:
240 return self.get_salaire_maximum_euros()
241 except DeviseException, e:
242 return e
243
244
245 ######################
246 # Comparaison de poste
247 ######################
248
249 def est_comparable(self):
250 """
251 Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
252 est comparable.
253 """
254 if self.comp_universite_min is None and \
255 self.comp_fonctionpub_min is None and \
256 self.comp_locale_min is None and \
257 self.comp_ong_min is None and \
258 self.comp_autre_min is None and \
259 self.comp_universite_max is None and \
260 self.comp_fonctionpub_max is None and \
261 self.comp_locale_max is None and \
262 self.comp_ong_max is None and \
263 self.comp_autre_max is None:
264 return False
265 else:
266 return True
267
268
269 def get_taux_comparaison(self):
270 try:
271 return rh.TauxChange.objects.filter(devise=self.devise_comparaison)[0].taux
272 except:
273 return 1
274
275 def get_comp_universite_min_euros(self):
276 return (float)(self.comp_universite_min) * self.get_taux_comparaison()
277
278 def get_comp_fonctionpub_min_euros(self):
279 return (float)(self.comp_fonctionpub_min) * self.get_taux_comparaison()
280
281 def get_comp_locale_min_euros(self):
282 return (float)(self.comp_locale_min) * self.get_taux_comparaison()
283
284 def get_comp_ong_min_euros(self):
285 return (float)(self.comp_ong_min) * self.get_taux_comparaison()
286
287 def get_comp_autre_min_euros(self):
288 return (float)(self.comp_autre_min) * self.get_taux_comparaison()
289
290 def get_comp_universite_max_euros(self):
291 return (float)(self.comp_universite_max) * self.get_taux_comparaison()
292
293 def get_comp_fonctionpub_max_euros(self):
294 return (float)(self.comp_fonctionpub_max) * self.get_taux_comparaison()
295
296 def get_comp_locale_max_euros(self):
297 return (float)(self.comp_locale_max) * self.get_taux_comparaison()
298
299 def get_comp_ong_max_euros(self):
300 return (float)(self.comp_ong_max) * self.get_taux_comparaison()
301
302 def get_comp_autre_max_euros(self):
303 return (float)(self.comp_autre_max) * self.get_taux_comparaison()
304
305
306 def __unicode__(self):
307 """
308 Cette fonction est consommatrice SQL car elle cherche les dossiers
309 qui ont été liés à celui-ci.
310 """
311 complement_nom_poste = self.get_complement_nom()
312 if complement_nom_poste is None:
313 complement_nom_poste = ""
314 data = (
315 self.implantation,
316 self.type_poste.nom,
317 self.nom,
318 )
319 return u'%s - %s (%s)' % data
320
321
322 # Tester l'enregistrement car les models.py sont importés au complet
323 if not reversion.is_registered(Poste):
324 reversion.register(Poste)
325
326
327 POSTE_FINANCEMENT_CHOICES = (
328 ('A', 'A - Frais de personnel'),
329 ('B', 'B - Projet(s)-Titre(s)'),
330 ('C', 'C - Autre')
331 )
332
333 class PosteFinancement(rh.PosteFinancement_):
334 pass
335
336 class PostePiece(rh.PostePiece_):
337 pass
338
339 class PosteComparaison(rh.PosteComparaison_):
340 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name=u'Statut', null=True, blank=True, )
341 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name=u'Classement', null=True, blank=True, )
342
343 ### EMPLOYÉ/PERSONNE
344
345 # TODO : migration pour m -> M, f -> F
346
347 GENRE_CHOICES = (
348 ('m', 'Homme'),
349 ('f', 'Femme'),
350 )
351
352 class Employe(models.Model):
353
354 # Modèle existant
355 id_rh = models.ForeignKey(rh.Employe, null=True, related_name='+',
356 verbose_name=u'Employé')
357 nom = models.CharField(max_length=255)
358 prenom = models.CharField(max_length=255, verbose_name=u'Prénom')
359 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
360
361 def __unicode__(self):
362 return u'%s %s' % (self.prenom, self.nom.upper())
363
364
365 ### DOSSIER
366
367 STATUT_RESIDENCE_CHOICES = (
368 ('local', 'Local'),
369 ('expat', 'Expatrié'),
370 )
371
372 COMPTE_COMPTA_CHOICES = (
373 ('coda', 'CODA'),
374 ('scs', 'SCS'),
375 ('aucun', 'Aucun'),
376 )
377
378 class Dossier(DossierWorkflow, rh.Dossier_):
379 poste = models.ForeignKey('Poste', db_column='poste', related_name='%(app_label)s_dossiers')
380 employe = models.ForeignKey('Employe', db_column='employe',
381 related_name='%(app_label)s_dossiers',
382 verbose_name=u"Employé")
383 organisme_bstg_autre = models.CharField(max_length=255,
384 verbose_name=u"Autre organisme",
385 help_text="indiquer l'organisme ici s'il n'est pas dans la liste",
386 null=True,
387 blank=True,)
388
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=u'Statut antérieur')
393 classement_anterieur = models.ForeignKey(
394 rh.Classement, related_name='+', null=True, blank=True,
395 verbose_name=u'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=u'Salaire précédent')
399 devise_anterieur = models.ForeignKey(rh.Devise, related_name='+',
400 null=True, blank=True)
401 type_contrat_anterieur = models.ForeignKey(rh.TypeContrat,
402 related_name='+', null=True, blank=True,
403 verbose_name=u'Type contrat antérieur', )
404
405 # Données du titulaire précédent
406 employe_anterieur = models.ForeignKey(
407 rh.Employe, related_name='+', null=True, blank=True,
408 verbose_name=u'Employé précédent')
409 statut_titulaire_anterieur = models.ForeignKey(
410 rh.Statut, related_name='+', null=True, blank=True,
411 verbose_name=u'Statut titulaire précédent')
412 classement_titulaire_anterieur = models.ForeignKey(
413 rh.Classement, related_name='+', null=True, blank=True,
414 verbose_name=u'Classement titulaire précédent')
415 salaire_titulaire_anterieur = models.DecimalField(
416 max_digits=12, decimal_places=2, default=None, null=True,
417 blank=True, verbose_name=u'Salaire titulaire précédent')
418 devise_titulaire_anterieur = models.ForeignKey(rh.Devise, related_name='+', null=True, blank=True)
419
420 # Rémunération
421 salaire = models.DecimalField(max_digits=13, decimal_places=2,
422 verbose_name=u'Salaire de base',
423 null=True, default=None)
424 devise = models.ForeignKey(rh.Devise, default=5, related_name='+')
425
426 # Contrat
427 type_contrat = models.ForeignKey(rh.TypeContrat, related_name='+')
428 contrat_date_debut = models.DateField(help_text=HELP_TEXT_DATE)
429 contrat_date_fin = models.DateField(null=True, blank=True,
430 help_text=HELP_TEXT_DATE)
431
432 # Justifications
433 justif_nouveau_statut_label = u'Justifier le statut que ce type de poste nécessite (national, expatrié, màd ou détachement)'
434 justif_nouveau_statut = models.TextField(verbose_name=justif_nouveau_statut_label, null=True, blank=True)
435 justif_nouveau_tmp_remplacement_label = u"Si l'employé effectue un remplacement temporaire, préciser"
436 justif_nouveau_tmp_remplacement = models.TextField(verbose_name=justif_nouveau_tmp_remplacement_label, null=True, blank=True)
437 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 "
438 justif_nouveau_salaire = models.TextField(verbose_name=justif_nouveau_salaire_label, null=True, blank=True)
439 justif_nouveau_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
440 justif_nouveau_commentaire = models.TextField(verbose_name=justif_nouveau_commentaire_label, null=True, blank=True)
441 justif_rempl_type_contrat_label = u"Changement de type de contrat, ex : d'un CDD en CDI"
442 justif_rempl_type_contrat = models.TextField(verbose_name=justif_rempl_type_contrat_label, null=True, blank=True)
443 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"
444 justif_rempl_statut_employe = models.TextField(verbose_name=justif_rempl_statut_employe_label, null=True, blank=True)
445 justif_rempl_evaluation_label = u"L'évaluation de l'employé est-elle favorable? Préciser"
446 justif_rempl_evaluation = models.TextField(verbose_name=justif_rempl_evaluation_label, null=True, blank=True)
447 justif_rempl_salaire_label = u"Si le salaire de l'employé est modifié et/ou ne correspond pas à son classement, justifier"
448 justif_rempl_salaire = models.TextField(verbose_name=justif_rempl_salaire_label, null=True, blank=True)
449 justif_rempl_commentaire_label = u"COMMENTAIRES ADDITIONNELS"
450 justif_rempl_commentaire = models.TextField(verbose_name=justif_rempl_commentaire_label, null=True, blank=True)
451
452 # Comptes
453 compte_compta = models.CharField(max_length=10, default='aucun',
454 verbose_name=u'Compte comptabilité',
455 choices=COMPTE_COMPTA_CHOICES)
456 compte_courriel = models.BooleanField()
457
458 # DAE numérisée
459 dae_numerisee = models.FileField(upload_to='dae/dae_numerisee', storage=UPLOAD_STORAGE,
460 blank=True, null=True, verbose_name="DAE numérisée")
461
462 # Managers
463 objects = DossierManager()
464
465 def __init__(self, *args, **kwargs):
466 # Bouchon pour créer une date fictive necessaire pour valider un dossier
467 # à cause de l'héritage
468 super(rh.Dossier_, self).__init__(*args, **kwargs)
469 super(DossierWorkflow, self).__init__(*args, **kwargs)
470 import datetime
471 self.date_debut = datetime.datetime.today()
472
473 def __unicode__(self):
474 return u'[%s] %s - %s' % (self.poste.implantation, self.poste.nom, self.employe)
475
476 def est_importe(self):
477 """Test si le dossier a déjà été importé"""
478 return dae.ImportDossier.objects.filter(dae=self).exists()
479
480 def importer(self):
481 if not self.poste.est_importe():
482 raise Exception('Le poste de cette DAE doît être importé')
483 return True
484
485 def taux_devise(self):
486 if self.devise.code == 'EUR':
487 return 1
488 liste_taux = self.devise.tauxchange_set.order_by('-annee')
489 if len(liste_taux) == 0:
490 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.poste.implantation))
491 else:
492 return liste_taux[0].taux
493
494 def get_salaire_anterieur_euros(self):
495 if self.devise_anterieur.code == 'EUR':
496 tx = 1
497 else:
498 liste_taux = self.devise_anterieur.tauxchange_set.order_by('-annee')
499 if len(liste_taux) == 0:
500 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_anterieur, self.poste.implantation))
501 tx = liste_taux[0].taux
502 return (float)(tx) * (float)(self.salaire_anterieur)
503
504 def get_salaire_titulaire_anterieur_euros(self):
505 if self.devise_titulaire_anterieur is None:
506 return None
507 if self.devise_titulaire_anterieur.code == 'EUR':
508 tx = 1
509 else:
510 liste_taux = self.devise_titulaire_anterieur.tauxchange_set.order_by('-annee')
511 if len(liste_taux) == 0:
512 raise DeviseException(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise_titulaire_anterieur, self.poste.implantation))
513 tx = liste_taux[0].taux
514 return (float)(tx) * (float)(self.salaire_titulaire_anterieur)
515
516 def get_salaire_euros(self):
517 tx = self.taux_devise()
518 return (float)(tx) * (float)(self.salaire)
519
520 def get_remunerations_brutes(self):
521 """
522 1 Salaire de base
523 3 Indemnité de base
524 4 Indemnité d'expatriation
525 5 Indemnité pour frais
526 6 Indemnité de logement
527 7 Indemnité de fonction
528 8 Indemnité de responsabilité
529 9 Indemnité de transport
530 10 Indemnité compensatrice
531 11 Indemnité de subsistance
532 12 Indemnité différentielle
533 13 Prime d'installation
534 14 Billet d'avion
535 15 Déménagement
536 16 Indemnité de départ
537 18 Prime de 13ième mois
538 19 Prime d'intérim
539 """
540 ids = [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
541 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
542
543 def get_charges_salariales(self):
544 """
545 20 Charges salariales ?
546 """
547 ids = [20, ]
548 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
549
550 def get_total_charges_salariales(self):
551 total = 0.0
552 for r in self.get_charges_salariales():
553 total += r.montant_euro()
554 return total
555
556 def get_charges_patronales(self):
557 """
558 17 Charges patronales
559 """
560 ids = [17, ]
561 return [r for r in self.dae_remunerations.all() if r.type_id in ids]
562
563 def get_total_charges_patronales(self):
564 total = 0.0
565 for r in self.get_charges_patronales():
566 total += r.montant_euro()
567 return total
568
569 def get_salaire_brut(self):
570 """
571 somme des rémuérations brutes
572 """
573 total = 0.0
574 for r in self.get_remunerations_brutes():
575 total += r.montant_euro()
576 return total
577
578 def get_salaire_net(self):
579 """
580 salaire brut - charges salariales
581 """
582 total_charges = 0.0
583 for r in self.get_charges_salariales():
584 total_charges += r.montant_euro()
585 return self.get_salaire_brut() - total_charges
586
587 def get_couts_auf(self):
588 """
589 salaire net + charges patronales
590 """
591 total_charges = 0.0
592 for r in self.get_charges_patronales():
593 total_charges += r.montant_euro()
594 return self.get_salaire_net() + total_charges
595
596 def get_remunerations_tierces(self):
597 """
598 2 Salaire MAD
599 """
600 return [r for r in self.dae_remunerations.all() if r.type_id in (2, )]
601
602 def get_total_remunerations_tierces(self):
603 total = 0.0
604 for r in self.get_remunerations_tierces():
605 total += r.montant_euro()
606 return total
607
608 def valide(self):
609 return self.etat in (DOSSIER_ETAT_REGION_FINALISATION,
610 DOSSIER_ETAT_DRH_FINALISATION,
611 DOSSIER_ETAT_FINALISE)
612
613
614 # Tester l'enregistrement car les models.py sont importés au complet
615 if not reversion.is_registered(Dossier):
616 reversion.register(Dossier)
617
618 class DossierPiece(rh.DossierPiece_):
619 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
620 Ex.: Lettre de motivation.
621 """
622 pass
623
624 class DossierComparaison(rh.DossierComparaison_):
625 """
626 Photo d'une comparaison salariale au moment de l'embauche.
627 """
628 statut = models.ForeignKey(rh.Statut, related_name='+', verbose_name='Statut', null=True, blank=True, )
629 classement = models.ForeignKey(rh.Classement, related_name='+', verbose_name='Classement', null=True, blank=True, )
630
631
632 ### RÉMUNÉRATION
633
634 class Remuneration(rh.Remuneration_):
635 pass
636
637 ### CONTRATS
638
639 class Contrat(rh.Contrat_):
640 pass
641
642 # modèle de liaison entre les systèmes
643
644 class ImportDossier(models.Model):
645 dae = models.ForeignKey('dae.Dossier', related_name='+')
646 rh = models.ForeignKey('rh.Dossier', related_name='+')
647
648 class ImportPoste(models.Model):
649 dae = models.ForeignKey('dae.Poste', related_name='+')
650 rh = models.ForeignKey('rh.Poste', related_name='+')