[#2658] Retrait du hack app_label() dans rh.models
[auf_rh_dae.git] / project / rh / models.py
1 # -=- encoding: utf-8 -=-
2
3 import datetime
4 from datetime import date
5 from decimal import Decimal
6
7 from auf.django.emploi.models import \
8 GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references
9 from auf.django.metadata.models import AUFMetadata
10 from auf.django.metadata.managers import NoDeleteManager
11 from auf.django.references import models as ref
12 from django.core.files.storage import FileSystemStorage
13 from django.db import models
14 from django.db.models import Q
15 from django.conf import settings
16
17 from change_list import \
18 RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \
19 STATUT_FUTUR
20 from managers import \
21 PosteManager, DossierManager, DossierComparaisonManager, \
22 PosteComparaisonManager, DeviseManager, ServiceManager, \
23 TypeRemunerationManager
24 from validators import validate_date_passee
25
26
27 # Constantes
28 HELP_TEXT_DATE = "format: jj-mm-aaaa"
29 REGIME_TRAVAIL_DEFAULT = Decimal('100.00')
30 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = Decimal('35.00')
31 REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT = \
32 "Saisir le nombre d'heure de travail à temps complet (100%), " \
33 "sans tenir compte du régime de travail"
34
35 # Upload de fichiers
36 storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
37 base_url=settings.PRIVE_MEDIA_URL)
38
39
40 def poste_piece_dispatch(instance, filename):
41 path = "%s/poste/%s/%s" % (
42 instance._meta.app_label, instance.poste_id, filename
43 )
44 return path
45
46
47 def dossier_piece_dispatch(instance, filename):
48 path = "%s/dossier/%s/%s" % (
49 instance._meta.app_label, instance.dossier_id, filename
50 )
51 return path
52
53
54 def employe_piece_dispatch(instance, filename):
55 path = "%s/employe/%s/%s" % (
56 instance._meta.app_label, instance.employe_id, filename
57 )
58 return path
59
60
61 def contrat_dispatch(instance, filename):
62 path = "%s/contrat/%s/%s" % (
63 instance._meta.app_label, instance.dossier_id, filename
64 )
65 return path
66
67
68 class DevisableMixin(object):
69
70 def get_annee_pour_taux_devise(self):
71 return datetime.datetime.now().year
72
73 def taux_devise(self, devise=None):
74 if devise is None:
75 devise = self.devise
76
77 if devise is None:
78 return None
79 if devise.code == "EUR":
80 return 1
81
82 annee = self.get_annee_pour_taux_devise()
83 taux = [
84 tc.taux
85 for tc in TauxChange.objects.filter(devise=devise, annee=annee)
86 ]
87 taux = set(taux)
88
89 if len(taux) == 0:
90 raise Exception(
91 u"Pas de taux pour %s en %s" % (devise.code, annee)
92 )
93
94 if len(taux) > 1:
95 raise Exception(u"Il existe plusieurs taux de %s en %s" %
96 (devise.code, annee))
97 else:
98 return list(taux)[0]
99
100 def montant_euros(self):
101 try:
102 taux = self.taux_devise()
103 except Exception, e:
104 return e
105 if not taux:
106 return None
107 return int(round(float(self.montant) * float(taux), 2))
108
109
110 class Commentaire(AUFMetadata):
111 texte = models.TextField()
112 owner = models.ForeignKey(
113 'auth.User', db_column='owner', related_name='+',
114 verbose_name=u"Commentaire de"
115 )
116
117 class Meta:
118 abstract = True
119 ordering = ['-date_creation']
120
121 def __unicode__(self):
122 return u'%s' % (self.texte)
123
124
125 ### POSTE
126
127 POSTE_APPEL_CHOICES = (
128 ('interne', 'Interne'),
129 ('externe', 'Externe'),
130 )
131
132
133 class Poste_(AUFMetadata):
134 """
135 Un Poste est un emploi (job) à combler dans une implantation.
136 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
137 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
138 """
139
140 objects = PosteManager()
141
142 # Identification
143 nom = models.CharField(u"Titre du poste", max_length=255)
144 nom_feminin = models.CharField(
145 u"Titre du poste (au féminin)", max_length=255, null=True
146 )
147 implantation = models.ForeignKey(
148 ref.Implantation,
149 help_text=u"Taper le nom de l'implantation ou sa région",
150 db_column='implantation', related_name='+'
151 )
152 type_poste = models.ForeignKey(
153 'TypePoste', db_column='type_poste',
154 help_text=u"Taper le nom du type de poste", related_name='+',
155 null=True, verbose_name=u"type de poste"
156 )
157 service = models.ForeignKey(
158 'Service', db_column='service', related_name='+',
159 verbose_name=u"direction/service/pôle support", null=True
160 )
161 responsable = models.ForeignKey(
162 'Poste', db_column='responsable',
163 related_name='+', null=True,
164 help_text=u"Taper le nom du poste ou du type de poste",
165 verbose_name=u"Poste du responsable"
166 )
167
168 # Contrat
169 regime_travail = models.DecimalField(
170 u"temps de travail", max_digits=12, decimal_places=2,
171 default=REGIME_TRAVAIL_DEFAULT, null=True,
172 help_text="% du temps complet"
173 )
174 regime_travail_nb_heure_semaine = models.DecimalField(
175 u"nb. heures par semaine", max_digits=12, decimal_places=2,
176 null=True, default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
177 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
178 )
179
180 # Recrutement
181 local = models.NullBooleanField(
182 u"local", default=True, null=True, blank=True
183 )
184 expatrie = models.NullBooleanField(
185 u"expatrié", default=False, null=True, blank=True
186 )
187 mise_a_disposition = models.NullBooleanField(
188 u"mise à disposition", null=True, default=False
189 )
190 appel = models.CharField(
191 u"Appel à candidature", max_length=10, null=True,
192 choices=POSTE_APPEL_CHOICES, default='interne'
193 )
194
195 # Rémunération
196 classement_min = models.ForeignKey(
197 'Classement', db_column='classement_min', related_name='+',
198 null=True, blank=True
199 )
200 classement_max = models.ForeignKey(
201 'Classement', db_column='classement_max', related_name='+',
202 null=True, blank=True
203 )
204 valeur_point_min = models.ForeignKey(
205 'ValeurPoint',
206 help_text=u"Taper le code ou le nom de l'implantation",
207 db_column='valeur_point_min', related_name='+', null=True,
208 blank=True
209 )
210 valeur_point_max = models.ForeignKey(
211 'ValeurPoint',
212 help_text=u"Taper le code ou le nom de l'implantation",
213 db_column='valeur_point_max', related_name='+', null=True,
214 blank=True
215 )
216 devise_min = models.ForeignKey(
217 'Devise', db_column='devise_min', null=True, related_name='+'
218 )
219 devise_max = models.ForeignKey(
220 'Devise', db_column='devise_max', null=True, related_name='+'
221 )
222 salaire_min = models.DecimalField(
223 max_digits=12, decimal_places=2, null=True, default=0
224 )
225 salaire_max = models.DecimalField(
226 max_digits=12, decimal_places=2, null=True, default=0
227 )
228 indemn_min = models.DecimalField(
229 max_digits=12, decimal_places=2, null=True, default=0
230 )
231 indemn_max = models.DecimalField(
232 max_digits=12, decimal_places=2, null=True, default=0
233 )
234 autre_min = models.DecimalField(
235 max_digits=12, decimal_places=2, null=True, default=0
236 )
237 autre_max = models.DecimalField(
238 max_digits=12, decimal_places=2, null=True, default=0
239 )
240
241 # Comparatifs de rémunération
242 devise_comparaison = models.ForeignKey(
243 'Devise', null=True, blank=True, db_column='devise_comparaison',
244 related_name='+'
245 )
246 comp_locale_min = models.DecimalField(
247 max_digits=12, decimal_places=2, null=True, blank=True
248 )
249 comp_locale_max = models.DecimalField(
250 max_digits=12, decimal_places=2, null=True, blank=True
251 )
252 comp_universite_min = models.DecimalField(
253 max_digits=12, decimal_places=2, null=True, blank=True
254 )
255 comp_universite_max = models.DecimalField(
256 max_digits=12, decimal_places=2, null=True, blank=True
257 )
258 comp_fonctionpub_min = models.DecimalField(
259 max_digits=12, decimal_places=2, null=True, blank=True
260 )
261 comp_fonctionpub_max = models.DecimalField(
262 max_digits=12, decimal_places=2, null=True, blank=True
263 )
264 comp_ong_min = models.DecimalField(
265 max_digits=12, decimal_places=2, null=True, blank=True
266 )
267 comp_ong_max = models.DecimalField(
268 max_digits=12, decimal_places=2, null=True, blank=True
269 )
270 comp_autre_min = models.DecimalField(
271 max_digits=12, decimal_places=2, null=True, blank=True
272 )
273 comp_autre_max = models.DecimalField(
274 max_digits=12, decimal_places=2, null=True, blank=True
275 )
276
277 # Justification
278 justification = models.TextField(null=True, blank=True)
279
280 # Autres Metadata
281 date_debut = models.DateField(
282 u"date de début", help_text=HELP_TEXT_DATE, null=True, blank=True
283 )
284 date_fin = models.DateField(
285 u"date de fin", help_text=HELP_TEXT_DATE, null=True, blank=True
286 )
287
288 class Meta:
289 abstract = True
290 ordering = ['implantation__nom', 'nom']
291 verbose_name = u"Poste"
292 verbose_name_plural = u"Postes"
293 ordering = ["nom"]
294
295 def __unicode__(self):
296 representation = u'%s - %s [%s]' % (
297 self.implantation, self.nom, self.id
298 )
299 return representation
300
301 prefix_implantation = "implantation__region"
302
303 def get_regions(self):
304 return [self.implantation.region]
305
306 def get_devise(self):
307 vp = ValeurPoint.objects.filter(
308 implantation=self.implantation, devise__archive=False
309 ).order_by('annee')
310 if len(vp) > 0:
311 return vp[0].devise
312 else:
313 return Devise.objects.get(code='EUR')
314
315
316 class Poste(Poste_):
317 __doc__ = Poste_.__doc__
318
319 # meta dématérialisation : pour permettre le filtrage
320 vacant = models.NullBooleanField(u"vacant", null=True, blank=True)
321
322 def is_vacant(self):
323 vacant = True
324 if self.occupe_par():
325 vacant = False
326 return vacant
327
328 def occupe_par(self):
329 """
330 Retourne la liste d'employé occupant ce poste.
331 Généralement, retourne une liste d'un élément.
332 Si poste inoccupé, retourne liste vide.
333 UTILISE pour mettre a jour le flag vacant
334 """
335 return [
336 d.employe for d in self.rh_dossiers
337 .filter(supprime=False)
338 .exclude(date_fin__lt=date.today())
339 ]
340
341
342 POSTE_FINANCEMENT_CHOICES = (
343 ('A', 'A - Frais de personnel'),
344 ('B', 'B - Projet(s)-Titre(s)'),
345 ('C', 'C - Autre')
346 )
347
348
349 class PosteFinancement_(models.Model):
350 """
351 Pour un Poste, structure d'informations décrivant comment on prévoit
352 financer ce Poste.
353 """
354 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
355 pourcentage = models.DecimalField(
356 max_digits=12, decimal_places=2,
357 help_text="ex.: 33.33 % (décimale avec point)"
358 )
359 commentaire = models.TextField(
360 help_text="Spécifiez la source de financement."
361 )
362
363 class Meta:
364 abstract = True
365 ordering = ['type']
366
367 def __unicode__(self):
368 return u'%s : %s %%' % (self.type, self.pourcentage)
369
370 def choix(self):
371 return u"%s" % dict(POSTE_FINANCEMENT_CHOICES)[self.type]
372
373
374 class PosteFinancement(PosteFinancement_):
375 poste = models.ForeignKey(
376 Poste, db_column='poste', related_name='rh_financements'
377 )
378
379
380 class PostePiece_(models.Model):
381 """
382 Documents relatifs au Poste.
383 Ex.: Description de poste
384 """
385 nom = models.CharField(u"Nom", max_length=255)
386 fichier = models.FileField(
387 u"Fichier", upload_to=poste_piece_dispatch, storage=storage_prive
388 )
389
390 class Meta:
391 abstract = True
392 ordering = ['nom']
393
394 def __unicode__(self):
395 return u'%s' % (self.nom)
396
397
398 class PostePiece(PostePiece_):
399 poste = models.ForeignKey(
400 Poste, db_column='poste', related_name='rh_pieces'
401 )
402
403
404 class PosteComparaison_(AUFMetadata, DevisableMixin):
405 """
406 De la même manière qu'un dossier, un poste peut-être comparé à un autre
407 poste.
408 """
409 objects = PosteComparaisonManager()
410
411 implantation = models.ForeignKey(
412 ref.Implantation, null=True, blank=True, related_name="+"
413 )
414 nom = models.CharField(u"Poste", max_length=255, null=True, blank=True)
415 montant = models.IntegerField(null=True)
416 devise = models.ForeignKey(
417 "Devise", related_name='+', null=True, blank=True
418 )
419
420 class Meta:
421 abstract = True
422
423 def __unicode__(self):
424 return self.nom
425
426
427 class PosteComparaison(PosteComparaison_):
428 poste = models.ForeignKey(
429 Poste, related_name='rh_comparaisons_internes'
430 )
431
432 objects = NoDeleteManager()
433
434
435 class PosteCommentaire(Commentaire):
436 poste = models.ForeignKey(
437 Poste, db_column='poste', related_name='commentaires'
438 )
439
440
441
442 ### EMPLOYÉ/PERSONNE
443
444 class Employe(AUFMetadata):
445 """
446 Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
447 Dossiers qu'il occupe ou a occupé de Postes.
448
449 Cette classe aurait pu avantageusement s'appeler Personne car la notion
450 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
451 """
452 # Identification
453 nom = models.CharField(max_length=255)
454 prenom = models.CharField(u"prénom", max_length=255)
455 nom_affichage = models.CharField(
456 u"nom d'affichage", max_length=255, null=True, blank=True
457 )
458 nationalite = models.ForeignKey(
459 ref.Pays, to_field='code', db_column='nationalite',
460 related_name='employes_nationalite', verbose_name=u"nationalité",
461 blank=True, null=True
462 )
463 date_naissance = models.DateField(
464 u"date de naissance", help_text=HELP_TEXT_DATE,
465 validators=[validate_date_passee], null=True, blank=True
466 )
467 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
468
469 # Infos personnelles
470 situation_famille = models.CharField(
471 u"situation familiale", max_length=1, choices=SITUATION_CHOICES,
472 null=True, blank=True
473 )
474 date_entree = models.DateField(
475 u"date d'entrée à l'AUF", help_text=HELP_TEXT_DATE, null=True,
476 blank=True
477 )
478
479 # Coordonnées
480 tel_domicile = models.CharField(
481 u"tél. domicile", max_length=255, null=True, blank=True
482 )
483 tel_cellulaire = models.CharField(
484 u"tél. cellulaire", max_length=255, null=True, blank=True
485 )
486 adresse = models.CharField(max_length=255, null=True, blank=True)
487 ville = models.CharField(max_length=255, null=True, blank=True)
488 province = models.CharField(max_length=255, null=True, blank=True)
489 code_postal = models.CharField(max_length=255, null=True, blank=True)
490 pays = models.ForeignKey(
491 ref.Pays, to_field='code', db_column='pays',
492 related_name='employes', null=True, blank=True
493 )
494
495 # meta dématérialisation : pour permettre le filtrage
496 nb_postes = models.IntegerField(u"nombre de postes", null=True, blank=True)
497
498 class Meta:
499 ordering = ['nom', 'prenom']
500 verbose_name = u"Employé"
501 verbose_name_plural = u"Employés"
502
503 def __unicode__(self):
504 return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
505
506 def civilite(self):
507 civilite = u''
508 if self.genre.upper() == u'M':
509 civilite = u'M.'
510 elif self.genre.upper() == u'F':
511 civilite = u'Mme'
512 return civilite
513
514 def url_photo(self):
515 """
516 Retourne l'URL du service retournant la photo de l'Employe.
517 Équivalent reverse url 'rh_photo' avec id en param.
518 """
519 from django.core.urlresolvers import reverse
520 return reverse('rh_photo', kwargs={'id': self.id})
521
522 def dossiers_passes(self):
523 params = {KEY_STATUT: STATUT_INACTIF, }
524 search = RechercheTemporelle(params, self.__class__)
525 search.purge_params(params)
526 q = search.get_q_temporel(self.rh_dossiers)
527 return self.rh_dossiers.filter(q)
528
529 def dossiers_futurs(self):
530 params = {KEY_STATUT: STATUT_FUTUR, }
531 search = RechercheTemporelle(params, self.__class__)
532 search.purge_params(params)
533 q = search.get_q_temporel(self.rh_dossiers)
534 return self.rh_dossiers.filter(q)
535
536 def dossiers_encours(self):
537 params = {KEY_STATUT: STATUT_ACTIF, }
538 search = RechercheTemporelle(params, self.__class__)
539 search.purge_params(params)
540 q = search.get_q_temporel(self.rh_dossiers)
541 return self.rh_dossiers.filter(q)
542
543 def postes_encours(self):
544 postes_encours = set()
545 for d in self.dossiers_encours():
546 postes_encours.add(d.poste)
547 return postes_encours
548
549 def poste_principal(self):
550 """
551 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
552 Idée derrière :
553 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
554 """
555 poste = Poste.objects.none()
556 try:
557 poste = self.dossiers_encours().order_by('date_debut')[0].poste
558 except:
559 pass
560 return poste
561
562 prefix_implantation = "rh_dossiers__poste__implantation__region"
563
564 def get_regions(self):
565 regions = []
566 for d in self.dossiers.all():
567 regions.append(d.poste.implantation.region)
568 return regions
569
570
571 class EmployePiece(models.Model):
572 """
573 Documents relatifs à un employé.
574 Ex.: CV...
575 """
576 employe = models.ForeignKey(
577 'Employe', db_column='employe', related_name="pieces",
578 verbose_name=u"employé"
579 )
580 nom = models.CharField(max_length=255)
581 fichier = models.FileField(
582 u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive
583 )
584
585 class Meta:
586 ordering = ['nom']
587 verbose_name = u"Employé pièce"
588 verbose_name_plural = u"Employé pièces"
589
590 def __unicode__(self):
591 return u'%s' % (self.nom)
592
593
594 class EmployeCommentaire(Commentaire):
595 employe = models.ForeignKey(
596 'Employe', db_column='employe', related_name='+'
597 )
598
599 class Meta:
600 verbose_name = u"Employé commentaire"
601 verbose_name_plural = u"Employé commentaires"
602
603
604 LIEN_PARENTE_CHOICES = (
605 ('Conjoint', 'Conjoint'),
606 ('Conjointe', 'Conjointe'),
607 ('Fille', 'Fille'),
608 ('Fils', 'Fils'),
609 )
610
611
612 class AyantDroit(AUFMetadata):
613 """
614 Personne en relation avec un Employe.
615 """
616 # Identification
617 nom = models.CharField(max_length=255)
618 prenom = models.CharField(u"prénom", max_length=255)
619 nom_affichage = models.CharField(
620 u"nom d'affichage", max_length=255, null=True, blank=True
621 )
622 nationalite = models.ForeignKey(
623 ref.Pays, to_field='code', db_column='nationalite',
624 related_name='ayantdroits_nationalite',
625 verbose_name=u"nationalité", null=True, blank=True
626 )
627 date_naissance = models.DateField(
628 u"Date de naissance", help_text=HELP_TEXT_DATE,
629 validators=[validate_date_passee], null=True, blank=True
630 )
631 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
632
633 # Relation
634 employe = models.ForeignKey(
635 'Employe', db_column='employe', related_name='ayantdroits',
636 verbose_name=u"Employé"
637 )
638 lien_parente = models.CharField(
639 u"lien de parenté", max_length=10, choices=LIEN_PARENTE_CHOICES,
640 null=True, blank=True
641 )
642
643 class Meta:
644 ordering = ['nom', ]
645 verbose_name = u"Ayant droit"
646 verbose_name_plural = u"Ayants droit"
647
648 def __unicode__(self):
649 return u'%s %s' % (self.nom.upper(), self.prenom, )
650
651 prefix_implantation = "employe__dossiers__poste__implantation__region"
652
653 def get_regions(self):
654 regions = []
655 for d in self.employe.dossiers.all():
656 regions.append(d.poste.implantation.region)
657 return regions
658
659
660 class AyantDroitCommentaire(Commentaire):
661 ayant_droit = models.ForeignKey(
662 'AyantDroit', db_column='ayant_droit', related_name='+'
663 )
664
665
666 ### DOSSIER
667
668 STATUT_RESIDENCE_CHOICES = (
669 ('local', 'Local'),
670 ('expat', 'Expatrié'),
671 )
672
673 COMPTE_COMPTA_CHOICES = (
674 ('coda', 'CODA'),
675 ('scs', 'SCS'),
676 ('aucun', 'Aucun'),
677 )
678
679
680 class Dossier_(AUFMetadata, DevisableMixin):
681 """
682 Le Dossier regroupe les informations relatives à l'occupation
683 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
684 par un Employe.
685
686 Plusieurs Contrats peuvent être associés au Dossier.
687 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
688 lequel aucun Dossier n'existe est un poste vacant.
689 """
690
691 objects = DossierManager()
692
693 # TODO: OneToOne ??
694 statut = models.ForeignKey('Statut', related_name='+', null=True)
695 organisme_bstg = models.ForeignKey(
696 'OrganismeBstg', db_column='organisme_bstg', related_name='+',
697 verbose_name=u"organisme",
698 help_text=(
699 u"Si détaché (DET) ou mis à disposition (MAD), "
700 u"préciser l'organisme."
701 ), null=True, blank=True
702 )
703
704 # Recrutement
705 remplacement = models.BooleanField(default=False)
706 remplacement_de = models.ForeignKey(
707 'self', related_name='+', help_text=u"Taper le nom de l'employé",
708 null=True, blank=True
709 )
710 statut_residence = models.CharField(
711 u"statut", max_length=10, default='local', null=True,
712 choices=STATUT_RESIDENCE_CHOICES
713 )
714
715 # Rémunération
716 classement = models.ForeignKey(
717 'Classement', db_column='classement', related_name='+', null=True,
718 blank=True
719 )
720 regime_travail = models.DecimalField(
721 u"régime de travail", max_digits=12, null=True, decimal_places=2,
722 default=REGIME_TRAVAIL_DEFAULT, help_text="% du temps complet"
723 )
724 regime_travail_nb_heure_semaine = models.DecimalField(
725 u"nb. heures par semaine", max_digits=12,
726 decimal_places=2, null=True,
727 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
728 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
729 )
730
731 # Occupation du Poste par cet Employe (anciennement "mandat")
732 date_debut = models.DateField(u"date de début d'occupation de poste")
733 date_fin = models.DateField(
734 u"Date de fin d'occupation de poste", null=True, blank=True
735 )
736
737 # Comptes
738 # TODO?
739
740 class Meta:
741 abstract = True
742 ordering = ['employe__nom', ]
743 verbose_name = u"Dossier"
744 verbose_name_plural = "Dossiers"
745
746 def salaire_theorique(self):
747 annee = date.today().year
748 coeff = self.classement.coefficient
749 implantation = self.poste.implantation
750 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
751
752 montant = coeff * point.valeur
753 devise = point.devise
754 return {'montant': montant, 'devise': devise}
755
756 def __unicode__(self):
757 poste = self.poste.nom
758 if self.employe.genre == 'F':
759 poste = self.poste.nom_feminin
760 return u'%s - %s' % (self.employe, poste)
761
762 prefix_implantation = "poste__implantation__region"
763
764 def get_regions(self):
765 return [self.poste.implantation.region]
766
767 def remunerations(self):
768 key = "%s_remunerations" % self._meta.app_label
769 remunerations = getattr(self, key)
770 return remunerations.all().order_by('-date_debut')
771
772 def remunerations_en_cours(self):
773 q = Q(date_fin__exact=None) | Q(date_fin__gt=datetime.date.today())
774 return self.remunerations().all().filter(q).order_by('date_debut')
775
776 def get_salaire(self):
777 try:
778 return [r for r in self.remunerations().order_by('-date_debut')
779 if r.type_id == 1][0]
780 except:
781 return None
782
783 def get_salaire_euros(self):
784 tx = self.taux_devise()
785 return (float)(tx) * (float)(self.salaire)
786
787 def get_remunerations_brutes(self):
788 """
789 1 Salaire de base
790 3 Indemnité de base
791 4 Indemnité d'expatriation
792 5 Indemnité pour frais
793 6 Indemnité de logement
794 7 Indemnité de fonction
795 8 Indemnité de responsabilité
796 9 Indemnité de transport
797 10 Indemnité compensatrice
798 11 Indemnité de subsistance
799 12 Indemnité différentielle
800 13 Prime d'installation
801 14 Billet d'avion
802 15 Déménagement
803 16 Indemnité de départ
804 18 Prime de 13ième mois
805 19 Prime d'intérim
806 """
807 ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
808 return [r for r in self.remunerations_en_cours().all()
809 if r.type_id in ids]
810
811 def get_charges_salariales(self):
812 """
813 20 Charges salariales ?
814 """
815 ids = [20]
816 return [r for r in self.remunerations_en_cours().all()
817 if r.type_id in ids]
818
819 def get_charges_patronales(self):
820 """
821 17 Charges patronales
822 """
823 ids = [17]
824 return [r for r in self.remunerations_en_cours().all()
825 if r.type_id in ids]
826
827 def get_remunerations_tierces(self):
828 """
829 2 Salaire MAD
830 """
831 return [r for r in self.remunerations_en_cours().all()
832 if r.type_id in (2,)]
833
834 # DEVISE LOCALE
835
836 def get_total_local_charges_salariales(self):
837 devise = self.poste.get_devise()
838 total = 0.0
839 for r in self.get_charges_salariales():
840 if r.devise != devise:
841 return None
842 total += float(r.montant)
843 return total
844
845 def get_total_local_charges_patronales(self):
846 devise = self.poste.get_devise()
847 total = 0.0
848 for r in self.get_charges_patronales():
849 if r.devise != devise:
850 return None
851 total += float(r.montant)
852 return total
853
854 def get_local_salaire_brut(self):
855 """
856 somme des rémuérations brutes
857 """
858 devise = self.poste.get_devise()
859 total = 0.0
860 for r in self.get_remunerations_brutes():
861 if r.devise != devise:
862 return None
863 total += float(r.montant)
864 return total
865
866 def get_local_salaire_net(self):
867 """
868 salaire brut - charges salariales
869 """
870 devise = self.poste.get_devise()
871 total_charges = 0.0
872 for r in self.get_charges_salariales():
873 if r.devise != devise:
874 return None
875 total_charges += float(r.montant)
876 return self.get_local_salaire_brut() - total_charges
877
878 def get_local_couts_auf(self):
879 """
880 salaire net + charges patronales
881 """
882 devise = self.poste.get_devise()
883 total_charges = 0.0
884 for r in self.get_charges_patronales():
885 if r.devise != devise:
886 return None
887 total_charges += float(r.montant)
888 return self.get_local_salaire_net() + total_charges
889
890 def get_total_local_remunerations_tierces(self):
891 devise = self.poste.get_devise()
892 total = 0.0
893 for r in self.get_remunerations_tierces():
894 if r.devise != devise:
895 return None
896 total += float(r.montant)
897 return total
898
899 # DEVISE EURO
900
901 def get_total_charges_salariales(self):
902 total = 0.0
903 for r in self.get_charges_salariales():
904 total += r.montant_euros()
905 return total
906
907 def get_total_charges_patronales(self):
908 total = 0.0
909 for r in self.get_charges_patronales():
910 total += r.montant_euros()
911 return total
912
913 def get_salaire_brut(self):
914 """
915 somme des rémuérations brutes
916 """
917 total = 0.0
918 for r in self.get_remunerations_brutes():
919 total += r.montant_euros()
920 return total
921
922 def get_salaire_net(self):
923 """
924 salaire brut - charges salariales
925 """
926 total_charges = 0.0
927 for r in self.get_charges_salariales():
928 total_charges += r.montant_euros()
929 return self.get_salaire_brut() - total_charges
930
931 def get_couts_auf(self):
932 """
933 salaire net + charges patronales
934 """
935 total_charges = 0.0
936 for r in self.get_charges_patronales():
937 total_charges += r.montant_euros()
938 return self.get_salaire_net() + total_charges
939
940 def get_total_remunerations_tierces(self):
941 total = 0.0
942 for r in self.get_remunerations_tierces():
943 total += r.montant_euros()
944 return total
945
946
947 class Dossier(Dossier_):
948 __doc__ = Dossier_.__doc__
949 poste = models.ForeignKey(
950 Poste, db_column='poste', related_name='rh_dossiers',
951 help_text=u"Taper le nom du poste ou du type de poste",
952 )
953 employe = models.ForeignKey(
954 'Employe', db_column='employe',
955 help_text=u"Taper le nom de l'employé",
956 related_name='rh_dossiers', verbose_name=u"employé"
957 )
958 principal = models.BooleanField(
959 u"Principal?", default=True,
960 help_text=(
961 u"Ce dossier est pour le principal poste occupé par l'employé"
962 )
963 )
964
965
966 class DossierPiece_(models.Model):
967 """
968 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
969 Ex.: Lettre de motivation.
970 """
971 nom = models.CharField(max_length=255)
972 fichier = models.FileField(
973 upload_to=dossier_piece_dispatch, storage=storage_prive
974 )
975
976 class Meta:
977 abstract = True
978 ordering = ['nom']
979
980 def __unicode__(self):
981 return u'%s' % (self.nom)
982
983
984 class DossierPiece(DossierPiece_):
985 dossier = models.ForeignKey(
986 Dossier, db_column='dossier', related_name='rh_dossierpieces'
987 )
988
989
990
991 class DossierCommentaire(Commentaire):
992 dossier = models.ForeignKey(
993 Dossier, db_column='dossier', related_name='commentaires'
994 )
995
996
997 class DossierComparaison_(models.Model, DevisableMixin):
998 """
999 Photo d'une comparaison salariale au moment de l'embauche.
1000 """
1001 objects = DossierComparaisonManager()
1002
1003 implantation = models.ForeignKey(
1004 ref.Implantation, related_name="+", null=True, blank=True
1005 )
1006 poste = models.CharField(max_length=255, null=True, blank=True)
1007 personne = models.CharField(max_length=255, null=True, blank=True)
1008 montant = models.IntegerField(null=True)
1009 devise = models.ForeignKey(
1010 'Devise', related_name='+', null=True, blank=True
1011 )
1012
1013 class Meta:
1014 abstract = True
1015
1016 def __unicode__(self):
1017 return "%s (%s)" % (self.poste, self.personne)
1018
1019
1020 class DossierComparaison(DossierComparaison_):
1021 dossier = models.ForeignKey(
1022 Dossier, related_name='rh_comparaisons'
1023 )
1024
1025
1026 ### RÉMUNÉRATION
1027
1028 class RemunerationMixin(AUFMetadata):
1029
1030 # Identification
1031 type = models.ForeignKey(
1032 'TypeRemuneration', db_column='type', related_name='+',
1033 verbose_name=u"type de rémunération"
1034 )
1035 type_revalorisation = models.ForeignKey(
1036 'TypeRevalorisation', db_column='type_revalorisation',
1037 related_name='+', verbose_name=u"type de revalorisation",
1038 null=True, blank=True
1039 )
1040 montant = models.DecimalField(
1041 null=True, blank=True,
1042 default=0, max_digits=12, decimal_places=2
1043 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1044 devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
1045
1046 # commentaire = precision
1047 commentaire = models.CharField(max_length=255, null=True, blank=True)
1048
1049 # date_debut = anciennement date_effectif
1050 date_debut = models.DateField(u"date de début", null=True, blank=True)
1051 date_fin = models.DateField(u"date de fin", null=True, blank=True)
1052
1053 class Meta:
1054 abstract = True
1055 ordering = ['type__nom', '-date_fin']
1056
1057 def __unicode__(self):
1058 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
1059
1060
1061 class Remuneration_(RemunerationMixin, DevisableMixin):
1062 """
1063 Structure de rémunération (données budgétaires) en situation normale
1064 pour un Dossier. Si un Evenement existe, utiliser la structure de
1065 rémunération EvenementRemuneration de cet événement.
1066 """
1067
1068 def montant_mois(self):
1069 return round(self.montant / 12, 2)
1070
1071 def montant_avec_regime(self):
1072 return round(self.montant * (self.dossier.regime_travail / 100), 2)
1073
1074 def montant_euro_mois(self):
1075 return round(self.montant_euros() / 12, 2)
1076
1077 def __unicode__(self):
1078 try:
1079 devise = self.devise.code
1080 except:
1081 devise = "???"
1082 return "%s %s" % (self.montant, devise)
1083
1084 class Meta:
1085 abstract = True
1086 verbose_name = u"Rémunération"
1087 verbose_name_plural = u"Rémunérations"
1088
1089
1090 class Remuneration(Remuneration_):
1091 dossier = models.ForeignKey(
1092 Dossier, db_column='dossier', related_name='rh_remunerations'
1093 )
1094
1095
1096 ### CONTRATS
1097
1098 class ContratManager(NoDeleteManager):
1099 def get_query_set(self):
1100 return super(ContratManager, self).get_query_set() \
1101 .select_related('dossier', 'dossier__poste')
1102
1103
1104 class Contrat_(AUFMetadata):
1105 """
1106 Document juridique qui encadre la relation de travail d'un Employe
1107 pour un Poste particulier. Pour un Dossier (qui documente cette
1108 relation de travail) plusieurs contrats peuvent être associés.
1109 """
1110 objects = ContratManager()
1111 type_contrat = models.ForeignKey(
1112 'TypeContrat', db_column='type_contrat',
1113 verbose_name=u'type de contrat', related_name='+'
1114 )
1115 date_debut = models.DateField(u"date de début")
1116 date_fin = models.DateField(u"date de fin", null=True, blank=True)
1117 fichier = models.FileField(
1118 upload_to=contrat_dispatch, storage=storage_prive, null=True,
1119 blank=True
1120 )
1121
1122 class Meta:
1123 abstract = True
1124 ordering = ['dossier__employe__nom']
1125 verbose_name = u"Contrat"
1126 verbose_name_plural = u"Contrats"
1127
1128 def __unicode__(self):
1129 return u'%s - %s' % (self.dossier, self.id)
1130
1131
1132 class Contrat(Contrat_):
1133 dossier = models.ForeignKey(
1134 Dossier, db_column='dossier', related_name='rh_contrats'
1135 )
1136
1137
1138 ### RÉFÉRENCES RH
1139
1140 class CategorieEmploi(AUFMetadata):
1141 """
1142 Catégorie utilisée dans la gestion des Postes.
1143 Catégorie supérieure à TypePoste.
1144 """
1145 nom = models.CharField(max_length=255)
1146
1147 class Meta:
1148 ordering = ('nom',)
1149 verbose_name = u"catégorie d'emploi"
1150 verbose_name_plural = u"catégories d'emploi"
1151
1152 def __unicode__(self):
1153 return self.nom
1154
1155
1156 class FamilleProfessionnelle(models.Model):
1157 """
1158 Famille professionnelle d'un poste.
1159 """
1160 nom = models.CharField(max_length=100)
1161
1162 class Meta:
1163 ordering = ('nom',)
1164 verbose_name = u'famille professionnelle'
1165 verbose_name_plural = u'familles professionnelles'
1166
1167 def __unicode__(self):
1168 return self.nom
1169
1170
1171 class TypePoste(AUFMetadata):
1172 """
1173 Catégorie de Poste.
1174 """
1175 nom = models.CharField(max_length=255)
1176 nom_feminin = models.CharField(u"nom féminin", max_length=255)
1177 is_responsable = models.BooleanField(
1178 u"poste de responsabilité", default=False
1179 )
1180 categorie_emploi = models.ForeignKey(
1181 CategorieEmploi, db_column='categorie_emploi', related_name='+',
1182 verbose_name=u"catégorie d'emploi"
1183 )
1184 famille_professionnelle = models.ForeignKey(
1185 FamilleProfessionnelle, related_name='types_de_poste',
1186 verbose_name=u"famille professionnelle", blank=True, null=True
1187 )
1188
1189 class Meta:
1190 ordering = ['nom']
1191 verbose_name = u"Type de poste"
1192 verbose_name_plural = u"Types de poste"
1193
1194 def __unicode__(self):
1195 return u'%s' % (self.nom)
1196
1197 TYPE_PAIEMENT_CHOICES = (
1198 (u'Régulier', u'Régulier'),
1199 (u'Ponctuel', u'Ponctuel'),
1200 )
1201
1202 NATURE_REMUNERATION_CHOICES = (
1203 (u'Accessoire', u'Accessoire'),
1204 (u'Charges', u'Charges'),
1205 (u'Indemnité', u'Indemnité'),
1206 (u'RAS', u'Rémunération autre source'),
1207 (u'Traitement', u'Traitement'),
1208 )
1209
1210
1211 class TypeRemuneration(AUFMetadata):
1212 """
1213 Catégorie de Remuneration.
1214 """
1215 objects = TypeRemunerationManager()
1216
1217 nom = models.CharField(max_length=255)
1218 type_paiement = models.CharField(
1219 u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
1220 )
1221 nature_remuneration = models.CharField(
1222 u"nature de la rémunération", max_length=30,
1223 choices=NATURE_REMUNERATION_CHOICES
1224 )
1225 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1226
1227 class Meta:
1228 ordering = ['nom']
1229 verbose_name = u"Type de rémunération"
1230 verbose_name_plural = u"Types de rémunération"
1231
1232 def __unicode__(self):
1233 if self.archive:
1234 archive = u"(archivé)"
1235 else:
1236 archive = ""
1237 return u'%s %s' % (self.nom, archive)
1238
1239
1240 class TypeRevalorisation(AUFMetadata):
1241 """
1242 Justification du changement de la Remuneration.
1243 (Actuellement utilisé dans aucun traitement informatique.)
1244 """
1245 nom = models.CharField(max_length=255)
1246
1247 class Meta:
1248 ordering = ['nom']
1249 verbose_name = u"Type de revalorisation"
1250 verbose_name_plural = u"Types de revalorisation"
1251
1252 def __unicode__(self):
1253 return u'%s' % (self.nom)
1254
1255
1256 class Service(AUFMetadata):
1257 """
1258 Unité administrative où les Postes sont rattachés.
1259 """
1260 objects = ServiceManager()
1261
1262 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1263 nom = models.CharField(max_length=255)
1264
1265 class Meta:
1266 ordering = ['nom']
1267 verbose_name = u"Service"
1268 verbose_name_plural = u"Services"
1269
1270 def __unicode__(self):
1271 if self.archive:
1272 archive = u"(archivé)"
1273 else:
1274 archive = ""
1275 return u'%s %s' % (self.nom, archive)
1276
1277
1278 TYPE_ORGANISME_CHOICES = (
1279 ('MAD', 'Mise à disposition'),
1280 ('DET', 'Détachement'),
1281 )
1282
1283
1284 class OrganismeBstg(AUFMetadata):
1285 """
1286 Organisation d'où provient un Employe mis à disposition (MAD) de
1287 ou détaché (DET) à l'AUF à titre gratuit.
1288
1289 (BSTG = bien et service à titre gratuit.)
1290 """
1291 nom = models.CharField(max_length=255)
1292 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
1293 pays = models.ForeignKey(ref.Pays, to_field='code',
1294 db_column='pays',
1295 related_name='organismes_bstg',
1296 null=True, blank=True)
1297
1298 class Meta:
1299 ordering = ['type', 'nom']
1300 verbose_name = u"Organisme BSTG"
1301 verbose_name_plural = u"Organismes BSTG"
1302
1303 def __unicode__(self):
1304 return u'%s (%s)' % (self.nom, self.get_type_display())
1305
1306 prefix_implantation = "pays__region"
1307
1308 def get_regions(self):
1309 return [self.pays.region]
1310
1311
1312 class Statut(AUFMetadata):
1313 """
1314 Statut de l'Employe dans le cadre d'un Dossier particulier.
1315 """
1316 # Identification
1317 code = models.CharField(
1318 max_length=25, unique=True,
1319 help_text=(
1320 u"Saisir un code court mais lisible pour ce statut : "
1321 u"le code est utilisé pour associer les statuts aux autres "
1322 u"données tout en demeurant plus lisible qu'un identifiant "
1323 u"numérique."
1324 )
1325 )
1326 nom = models.CharField(max_length=255)
1327
1328 class Meta:
1329 ordering = ['code']
1330 verbose_name = u"Statut d'employé"
1331 verbose_name_plural = u"Statuts d'employé"
1332
1333 def __unicode__(self):
1334 return u'%s : %s' % (self.code, self.nom)
1335
1336
1337 TYPE_CLASSEMENT_CHOICES = (
1338 ('S', 'S -Soutien'),
1339 ('T', 'T - Technicien'),
1340 ('P', 'P - Professionel'),
1341 ('C', 'C - Cadre'),
1342 ('D', 'D - Direction'),
1343 ('SO', 'SO - Sans objet [expatriés]'),
1344 ('HG', 'HG - Hors grille [direction]'),
1345 )
1346
1347
1348 class ClassementManager(models.Manager):
1349 """
1350 Ordonner les spcéfiquement les classements.
1351 """
1352 def get_query_set(self):
1353 qs = super(self.__class__, self).get_query_set()
1354 qs = qs.extra(select={
1355 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1356 })
1357 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
1358 return qs.all()
1359
1360
1361 class Classement_(AUFMetadata):
1362 """
1363 Éléments de classement de la
1364 "Grille générique de classement hiérarchique".
1365
1366 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1367 classement dans la grille. Le classement donne le coefficient utilisé dans:
1368
1369 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1370 """
1371 objects = ClassementManager()
1372
1373 # Identification
1374 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1375 echelon = models.IntegerField(u"échelon", blank=True, default=0)
1376 degre = models.IntegerField(u"degré", blank=True, default=0)
1377 coefficient = models.FloatField(u"coefficient", default=0, null=True)
1378
1379 # Méta
1380 # annee # au lieu de date_debut et date_fin
1381 commentaire = models.TextField(null=True, blank=True)
1382
1383 class Meta:
1384 abstract = True
1385 ordering = ['type', 'echelon', 'degre', 'coefficient']
1386 verbose_name = u"Classement"
1387 verbose_name_plural = u"Classements"
1388
1389 def __unicode__(self):
1390 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
1391
1392
1393 class Classement(Classement_):
1394 __doc__ = Classement_.__doc__
1395
1396
1397 class TauxChange_(AUFMetadata):
1398 """
1399 Taux de change de la devise vers l'euro (EUR)
1400 pour chaque année budgétaire.
1401 """
1402 # Identification
1403 devise = models.ForeignKey('Devise', db_column='devise')
1404 annee = models.IntegerField(u"année")
1405 taux = models.FloatField(u"taux vers l'euro")
1406
1407 class Meta:
1408 abstract = True
1409 ordering = ['-annee', 'devise__code']
1410 verbose_name = u"Taux de change"
1411 verbose_name_plural = u"Taux de change"
1412
1413 def __unicode__(self):
1414 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1415
1416
1417 class TauxChange(TauxChange_):
1418 __doc__ = TauxChange_.__doc__
1419
1420
1421 class ValeurPointManager(NoDeleteManager):
1422
1423 def get_query_set(self):
1424 return super(ValeurPointManager, self).get_query_set() \
1425 .select_related('devise', 'implantation')
1426
1427
1428 class ValeurPoint_(AUFMetadata):
1429 """
1430 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1431 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1432 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1433
1434 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1435 """
1436
1437 actuelles = ValeurPointManager()
1438
1439 valeur = models.FloatField(null=True)
1440 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
1441 implantation = models.ForeignKey(ref.Implantation,
1442 db_column='implantation',
1443 related_name='%(app_label)s_valeur_point')
1444 # Méta
1445 annee = models.IntegerField()
1446
1447 class Meta:
1448 ordering = ['-annee', 'implantation__nom']
1449 abstract = True
1450 verbose_name = u"Valeur du point"
1451 verbose_name_plural = u"Valeurs du point"
1452
1453 def __unicode__(self):
1454 return u'%s %s %s [%s] %s' % (
1455 self.devise.code, self.annee, self.valeur,
1456 self.implantation.nom_court, self.devise.nom
1457 )
1458
1459
1460 class ValeurPoint(ValeurPoint_):
1461 __doc__ = ValeurPoint_.__doc__
1462
1463
1464 class Devise(AUFMetadata):
1465 """
1466 Devise monétaire.
1467 """
1468 objects = DeviseManager()
1469
1470 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1471 code = models.CharField(max_length=10, unique=True)
1472 nom = models.CharField(max_length=255)
1473
1474 class Meta:
1475 ordering = ['code']
1476 verbose_name = u"Devise"
1477 verbose_name_plural = u"Devises"
1478
1479 def __unicode__(self):
1480 return u'%s - %s' % (self.code, self.nom)
1481
1482
1483 class TypeContrat(AUFMetadata):
1484 """
1485 Type de contrat.
1486 """
1487 nom = models.CharField(max_length=255)
1488 nom_long = models.CharField(max_length=255)
1489
1490 class Meta:
1491 ordering = ['nom']
1492 verbose_name = u"Type de contrat"
1493 verbose_name_plural = u"Types de contrat"
1494
1495 def __unicode__(self):
1496 return u'%s' % (self.nom)
1497
1498
1499 ### AUTRES
1500
1501 class ResponsableImplantationProxy(ref.Implantation):
1502
1503 class Meta:
1504 proxy = True
1505 verbose_name = u"Responsable d'implantation"
1506 verbose_name_plural = u"Responsables d'implantation"
1507
1508
1509 class ResponsableImplantation(models.Model):
1510 """
1511 Le responsable d'une implantation.
1512 Anciennement géré sur le Dossier du responsable.
1513 """
1514 employe = models.ForeignKey(
1515 'Employe', db_column='employe', related_name='+', null=True,
1516 blank=True
1517 )
1518 implantation = models.OneToOneField(
1519 "ResponsableImplantationProxy", db_column='implantation',
1520 related_name='responsable', unique=True
1521 )
1522
1523 def __unicode__(self):
1524 return u'%s : %s' % (self.implantation, self.employe)
1525
1526 class Meta:
1527 ordering = ['implantation__nom']
1528 verbose_name = "Responsable d'implantation"
1529 verbose_name_plural = "Responsables d'implantation"