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