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