conflit réglé merge master et regionalisation
[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='%(app_label)s_postes',
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 courriel_perso = models.EmailField(
496 u'adresse courriel personnelle', blank=True
497 )
498
499 # meta dématérialisation : pour permettre le filtrage
500 nb_postes = models.IntegerField(u"nombre de postes", null=True, blank=True)
501
502 class Meta:
503 ordering = ['nom', 'prenom']
504 verbose_name = u"Employé"
505 verbose_name_plural = u"Employés"
506
507 def __unicode__(self):
508 return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
509
510 def civilite(self):
511 civilite = u''
512 if self.genre.upper() == u'M':
513 civilite = u'M.'
514 elif self.genre.upper() == u'F':
515 civilite = u'Mme'
516 return civilite
517
518 def url_photo(self):
519 """
520 Retourne l'URL du service retournant la photo de l'Employe.
521 Équivalent reverse url 'rh_photo' avec id en param.
522 """
523 from django.core.urlresolvers import reverse
524 return reverse('rh_photo', kwargs={'id': self.id})
525
526 def dossiers_passes(self):
527 params = {KEY_STATUT: STATUT_INACTIF, }
528 search = RechercheTemporelle(params, self.__class__)
529 search.purge_params(params)
530 q = search.get_q_temporel(self.rh_dossiers)
531 return self.rh_dossiers.filter(q)
532
533 def dossiers_futurs(self):
534 params = {KEY_STATUT: STATUT_FUTUR, }
535 search = RechercheTemporelle(params, self.__class__)
536 search.purge_params(params)
537 q = search.get_q_temporel(self.rh_dossiers)
538 return self.rh_dossiers.filter(q)
539
540 def dossiers_encours(self):
541 params = {KEY_STATUT: STATUT_ACTIF, }
542 search = RechercheTemporelle(params, self.__class__)
543 search.purge_params(params)
544 q = search.get_q_temporel(self.rh_dossiers)
545 return self.rh_dossiers.filter(q)
546
547 def postes_encours(self):
548 postes_encours = set()
549 for d in self.dossiers_encours():
550 postes_encours.add(d.poste)
551 return postes_encours
552
553 def poste_principal(self):
554 """
555 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
556 Idée derrière :
557 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
558 """
559 poste = Poste.objects.none()
560 try:
561 poste = self.dossiers_encours().order_by('date_debut')[0].poste
562 except:
563 pass
564 return poste
565
566 prefix_implantation = "rh_dossiers__poste__implantation__region"
567
568 def get_regions(self):
569 regions = []
570 for d in self.dossiers.all():
571 regions.append(d.poste.implantation.region)
572 return regions
573
574
575 class EmployePiece(models.Model):
576 """
577 Documents relatifs à un employé.
578 Ex.: CV...
579 """
580 employe = models.ForeignKey(
581 'Employe', db_column='employe', related_name="pieces",
582 verbose_name=u"employé"
583 )
584 nom = models.CharField(max_length=255)
585 fichier = models.FileField(
586 u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive
587 )
588
589 class Meta:
590 ordering = ['nom']
591 verbose_name = u"Employé pièce"
592 verbose_name_plural = u"Employé pièces"
593
594 def __unicode__(self):
595 return u'%s' % (self.nom)
596
597
598 class EmployeCommentaire(Commentaire):
599 employe = models.ForeignKey(
600 'Employe', db_column='employe', related_name='+'
601 )
602
603 class Meta:
604 verbose_name = u"Employé commentaire"
605 verbose_name_plural = u"Employé commentaires"
606
607
608 LIEN_PARENTE_CHOICES = (
609 ('Conjoint', 'Conjoint'),
610 ('Conjointe', 'Conjointe'),
611 ('Fille', 'Fille'),
612 ('Fils', 'Fils'),
613 )
614
615
616 class AyantDroit(AUFMetadata):
617 """
618 Personne en relation avec un Employe.
619 """
620 # Identification
621 nom = models.CharField(max_length=255)
622 prenom = models.CharField(u"prénom", max_length=255)
623 nom_affichage = models.CharField(
624 u"nom d'affichage", max_length=255, null=True, blank=True
625 )
626 nationalite = models.ForeignKey(
627 ref.Pays, to_field='code', db_column='nationalite',
628 related_name='ayantdroits_nationalite',
629 verbose_name=u"nationalité", null=True, blank=True
630 )
631 date_naissance = models.DateField(
632 u"Date de naissance", help_text=HELP_TEXT_DATE,
633 validators=[validate_date_passee], null=True, blank=True
634 )
635 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
636
637 # Relation
638 employe = models.ForeignKey(
639 'Employe', db_column='employe', related_name='ayantdroits',
640 verbose_name=u"Employé"
641 )
642 lien_parente = models.CharField(
643 u"lien de parenté", max_length=10, choices=LIEN_PARENTE_CHOICES,
644 null=True, blank=True
645 )
646
647 class Meta:
648 ordering = ['nom', ]
649 verbose_name = u"Ayant droit"
650 verbose_name_plural = u"Ayants droit"
651
652 def __unicode__(self):
653 return u'%s %s' % (self.nom.upper(), self.prenom, )
654
655 prefix_implantation = "employe__dossiers__poste__implantation__region"
656
657 def get_regions(self):
658 regions = []
659 for d in self.employe.dossiers.all():
660 regions.append(d.poste.implantation.region)
661 return regions
662
663
664 class AyantDroitCommentaire(Commentaire):
665 ayant_droit = models.ForeignKey(
666 'AyantDroit', db_column='ayant_droit', related_name='+'
667 )
668
669
670 ### DOSSIER
671
672 STATUT_RESIDENCE_CHOICES = (
673 ('local', 'Local'),
674 ('expat', 'Expatrié'),
675 )
676
677 COMPTE_COMPTA_CHOICES = (
678 ('coda', 'CODA'),
679 ('scs', 'SCS'),
680 ('aucun', 'Aucun'),
681 )
682
683
684 class Dossier_(AUFMetadata, DevisableMixin):
685 """
686 Le Dossier regroupe les informations relatives à l'occupation
687 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
688 par un Employe.
689
690 Plusieurs Contrats peuvent être associés au Dossier.
691 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
692 lequel aucun Dossier n'existe est un poste vacant.
693 """
694
695 objects = DossierManager()
696
697 # TODO: OneToOne ??
698 statut = models.ForeignKey('Statut', related_name='+', null=True)
699 organisme_bstg = models.ForeignKey(
700 'OrganismeBstg', db_column='organisme_bstg', related_name='+',
701 verbose_name=u"organisme",
702 help_text=(
703 u"Si détaché (DET) ou mis à disposition (MAD), "
704 u"préciser l'organisme."
705 ), null=True, blank=True
706 )
707
708 # Recrutement
709 remplacement = models.BooleanField(default=False)
710 remplacement_de = models.ForeignKey(
711 'self', related_name='+', help_text=u"Taper le nom de l'employé",
712 null=True, blank=True
713 )
714 statut_residence = models.CharField(
715 u"statut", max_length=10, default='local', null=True,
716 choices=STATUT_RESIDENCE_CHOICES
717 )
718
719 # Rémunération
720 classement = models.ForeignKey(
721 'Classement', db_column='classement', related_name='+', null=True,
722 blank=True
723 )
724 regime_travail = models.DecimalField(
725 u"régime de travail", max_digits=12, null=True, decimal_places=2,
726 default=REGIME_TRAVAIL_DEFAULT, help_text="% du temps complet"
727 )
728 regime_travail_nb_heure_semaine = models.DecimalField(
729 u"nb. heures par semaine", max_digits=12,
730 decimal_places=2, null=True,
731 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
732 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
733 )
734
735 # Occupation du Poste par cet Employe (anciennement "mandat")
736 date_debut = models.DateField(u"date de début d'occupation de poste")
737 date_fin = models.DateField(
738 u"Date de fin d'occupation de poste", null=True, blank=True
739 )
740
741 # Comptes
742 # TODO?
743
744 class Meta:
745 abstract = True
746 ordering = ['employe__nom', ]
747 verbose_name = u"Dossier"
748 verbose_name_plural = "Dossiers"
749
750 def salaire_theorique(self):
751 annee = date.today().year
752 coeff = self.classement.coefficient
753 implantation = self.poste.implantation
754 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
755
756 montant = coeff * point.valeur
757 devise = point.devise
758 return {'montant': montant, 'devise': devise}
759
760 def __unicode__(self):
761 poste = self.poste.nom
762 if self.employe.genre == 'F':
763 poste = self.poste.nom_feminin
764 return u'%s - %s' % (self.employe, poste)
765
766 prefix_implantation = "poste__implantation__region"
767
768 def get_regions(self):
769 return [self.poste.implantation.region]
770
771 def remunerations(self):
772 key = "%s_remunerations" % self._meta.app_label
773 remunerations = getattr(self, key)
774 return remunerations.all().order_by('-date_debut')
775
776 def remunerations_en_cours(self):
777 q = Q(date_fin__exact=None) | Q(date_fin__gt=datetime.date.today())
778 return self.remunerations().all().filter(q).order_by('date_debut')
779
780 def get_salaire(self):
781 try:
782 return [r for r in self.remunerations().order_by('-date_debut')
783 if r.type_id == 1][0]
784 except:
785 return None
786
787 def get_salaire_euros(self):
788 tx = self.taux_devise()
789 return (float)(tx) * (float)(self.salaire)
790
791 def get_remunerations_brutes(self):
792 """
793 1 Salaire de base
794 3 Indemnité de base
795 4 Indemnité d'expatriation
796 5 Indemnité pour frais
797 6 Indemnité de logement
798 7 Indemnité de fonction
799 8 Indemnité de responsabilité
800 9 Indemnité de transport
801 10 Indemnité compensatrice
802 11 Indemnité de subsistance
803 12 Indemnité différentielle
804 13 Prime d'installation
805 14 Billet d'avion
806 15 Déménagement
807 16 Indemnité de départ
808 18 Prime de 13ième mois
809 19 Prime d'intérim
810 """
811 ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
812 return [r for r in self.remunerations_en_cours().all()
813 if r.type_id in ids]
814
815 def get_charges_salariales(self):
816 """
817 20 Charges salariales ?
818 """
819 ids = [20]
820 return [r for r in self.remunerations_en_cours().all()
821 if r.type_id in ids]
822
823 def get_charges_patronales(self):
824 """
825 17 Charges patronales
826 """
827 ids = [17]
828 return [r for r in self.remunerations_en_cours().all()
829 if r.type_id in ids]
830
831 def get_remunerations_tierces(self):
832 """
833 2 Salaire MAD
834 """
835 return [r for r in self.remunerations_en_cours().all()
836 if r.type_id in (2,)]
837
838 # DEVISE LOCALE
839
840 def get_total_local_charges_salariales(self):
841 devise = self.poste.get_devise()
842 total = 0.0
843 for r in self.get_charges_salariales():
844 if r.devise != devise:
845 return None
846 total += float(r.montant)
847 return total
848
849 def get_total_local_charges_patronales(self):
850 devise = self.poste.get_devise()
851 total = 0.0
852 for r in self.get_charges_patronales():
853 if r.devise != devise:
854 return None
855 total += float(r.montant)
856 return total
857
858 def get_local_salaire_brut(self):
859 """
860 somme des rémuérations brutes
861 """
862 devise = self.poste.get_devise()
863 total = 0.0
864 for r in self.get_remunerations_brutes():
865 if r.devise != devise:
866 return None
867 total += float(r.montant)
868 return total
869
870 def get_local_salaire_net(self):
871 """
872 salaire brut - charges salariales
873 """
874 devise = self.poste.get_devise()
875 total_charges = 0.0
876 for r in self.get_charges_salariales():
877 if r.devise != devise:
878 return None
879 total_charges += float(r.montant)
880 return self.get_local_salaire_brut() - total_charges
881
882 def get_local_couts_auf(self):
883 """
884 salaire net + charges patronales
885 """
886 devise = self.poste.get_devise()
887 total_charges = 0.0
888 for r in self.get_charges_patronales():
889 if r.devise != devise:
890 return None
891 total_charges += float(r.montant)
892 return self.get_local_salaire_net() + total_charges
893
894 def get_total_local_remunerations_tierces(self):
895 devise = self.poste.get_devise()
896 total = 0.0
897 for r in self.get_remunerations_tierces():
898 if r.devise != devise:
899 return None
900 total += float(r.montant)
901 return total
902
903 # DEVISE EURO
904
905 def get_total_charges_salariales(self):
906 total = 0.0
907 for r in self.get_charges_salariales():
908 total += r.montant_euros()
909 return total
910
911 def get_total_charges_patronales(self):
912 total = 0.0
913 for r in self.get_charges_patronales():
914 total += r.montant_euros()
915 return total
916
917 def get_salaire_brut(self):
918 """
919 somme des rémuérations brutes
920 """
921 total = 0.0
922 for r in self.get_remunerations_brutes():
923 total += r.montant_euros()
924 return total
925
926 def get_salaire_net(self):
927 """
928 salaire brut - charges salariales
929 """
930 total_charges = 0.0
931 for r in self.get_charges_salariales():
932 total_charges += r.montant_euros()
933 return self.get_salaire_brut() - total_charges
934
935 def get_couts_auf(self):
936 """
937 salaire net + charges patronales
938 """
939 total_charges = 0.0
940 for r in self.get_charges_patronales():
941 total_charges += r.montant_euros()
942 return self.get_salaire_net() + total_charges
943
944 def get_total_remunerations_tierces(self):
945 total = 0.0
946 for r in self.get_remunerations_tierces():
947 total += r.montant_euros()
948 return total
949
950 def actif(self):
951 today = date.today()
952 return (self.date_debut is None or self.date_debut <= today) \
953 and (self.date_fin is None or self.date_fin >= today) \
954 and not (self.date_fin is None and self.date_debut is None)
955
956
957 class Dossier(Dossier_):
958 __doc__ = Dossier_.__doc__
959 poste = models.ForeignKey(
960 Poste, db_column='poste', related_name='rh_dossiers',
961 help_text=u"Taper le nom du poste ou du type de poste",
962 )
963 employe = models.ForeignKey(
964 'Employe', db_column='employe',
965 help_text=u"Taper le nom de l'employé",
966 related_name='rh_dossiers', verbose_name=u"employé"
967 )
968 principal = models.BooleanField(
969 u"dossier principal", default=True,
970 help_text=(
971 u"Ce dossier est pour le principal poste occupé par l'employé"
972 )
973 )
974
975
976 class DossierPiece_(models.Model):
977 """
978 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
979 Ex.: Lettre de motivation.
980 """
981 nom = models.CharField(max_length=255)
982 fichier = models.FileField(
983 upload_to=dossier_piece_dispatch, storage=storage_prive
984 )
985
986 class Meta:
987 abstract = True
988 ordering = ['nom']
989
990 def __unicode__(self):
991 return u'%s' % (self.nom)
992
993
994 class DossierPiece(DossierPiece_):
995 dossier = models.ForeignKey(
996 Dossier, db_column='dossier', related_name='rh_dossierpieces'
997 )
998
999
1000
1001 class DossierCommentaire(Commentaire):
1002 dossier = models.ForeignKey(
1003 Dossier, db_column='dossier', related_name='commentaires'
1004 )
1005
1006
1007 class DossierComparaison_(models.Model, DevisableMixin):
1008 """
1009 Photo d'une comparaison salariale au moment de l'embauche.
1010 """
1011 objects = DossierComparaisonManager()
1012
1013 implantation = models.ForeignKey(
1014 ref.Implantation, related_name="+", null=True, blank=True
1015 )
1016 poste = models.CharField(max_length=255, null=True, blank=True)
1017 personne = models.CharField(max_length=255, null=True, blank=True)
1018 montant = models.IntegerField(null=True)
1019 devise = models.ForeignKey(
1020 'Devise', related_name='+', null=True, blank=True
1021 )
1022
1023 class Meta:
1024 abstract = True
1025
1026 def __unicode__(self):
1027 return "%s (%s)" % (self.poste, self.personne)
1028
1029
1030 class DossierComparaison(DossierComparaison_):
1031 dossier = models.ForeignKey(
1032 Dossier, related_name='rh_comparaisons'
1033 )
1034
1035
1036 ### RÉMUNÉRATION
1037
1038 class RemunerationMixin(AUFMetadata):
1039
1040 # Identification
1041 type = models.ForeignKey(
1042 'TypeRemuneration', db_column='type', related_name='+',
1043 verbose_name=u"type de rémunération"
1044 )
1045 type_revalorisation = models.ForeignKey(
1046 'TypeRevalorisation', db_column='type_revalorisation',
1047 related_name='+', verbose_name=u"type de revalorisation",
1048 null=True, blank=True
1049 )
1050 montant = models.DecimalField(
1051 null=True, blank=True,
1052 default=0, max_digits=12, decimal_places=2
1053 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1054 devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
1055
1056 # commentaire = precision
1057 commentaire = models.CharField(max_length=255, null=True, blank=True)
1058
1059 # date_debut = anciennement date_effectif
1060 date_debut = models.DateField(u"date de début", null=True, blank=True)
1061 date_fin = models.DateField(u"date de fin", null=True, blank=True)
1062
1063 class Meta:
1064 abstract = True
1065 ordering = ['type__nom', '-date_fin']
1066
1067 def __unicode__(self):
1068 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
1069
1070
1071 class Remuneration_(RemunerationMixin, DevisableMixin):
1072 """
1073 Structure de rémunération (données budgétaires) en situation normale
1074 pour un Dossier. Si un Evenement existe, utiliser la structure de
1075 rémunération EvenementRemuneration de cet événement.
1076 """
1077
1078 def montant_mois(self):
1079 return round(self.montant / 12, 2)
1080
1081 def montant_avec_regime(self):
1082 return round(self.montant * (self.dossier.regime_travail / 100), 2)
1083
1084 def montant_euro_mois(self):
1085 return round(self.montant_euros() / 12, 2)
1086
1087 def __unicode__(self):
1088 try:
1089 devise = self.devise.code
1090 except:
1091 devise = "???"
1092 return "%s %s" % (self.montant, devise)
1093
1094 class Meta:
1095 abstract = True
1096 verbose_name = u"Rémunération"
1097 verbose_name_plural = u"Rémunérations"
1098
1099
1100 class Remuneration(Remuneration_):
1101 dossier = models.ForeignKey(
1102 Dossier, db_column='dossier', related_name='rh_remunerations'
1103 )
1104
1105
1106 ### CONTRATS
1107
1108 class ContratManager(NoDeleteManager):
1109 def get_query_set(self):
1110 return super(ContratManager, self).get_query_set() \
1111 .select_related('dossier', 'dossier__poste')
1112
1113
1114 class Contrat_(AUFMetadata):
1115 """
1116 Document juridique qui encadre la relation de travail d'un Employe
1117 pour un Poste particulier. Pour un Dossier (qui documente cette
1118 relation de travail) plusieurs contrats peuvent être associés.
1119 """
1120 objects = ContratManager()
1121 type_contrat = models.ForeignKey(
1122 'TypeContrat', db_column='type_contrat',
1123 verbose_name=u'type de contrat', related_name='+'
1124 )
1125 date_debut = models.DateField(u"date de début")
1126 date_fin = models.DateField(u"date de fin", null=True, blank=True)
1127 fichier = models.FileField(
1128 upload_to=contrat_dispatch, storage=storage_prive, null=True,
1129 blank=True
1130 )
1131
1132 class Meta:
1133 abstract = True
1134 ordering = ['dossier__employe__nom']
1135 verbose_name = u"Contrat"
1136 verbose_name_plural = u"Contrats"
1137
1138 def __unicode__(self):
1139 return u'%s - %s' % (self.dossier, self.id)
1140
1141
1142 class Contrat(Contrat_):
1143 dossier = models.ForeignKey(
1144 Dossier, db_column='dossier', related_name='rh_contrats'
1145 )
1146
1147
1148 ### RÉFÉRENCES RH
1149
1150 class CategorieEmploi(AUFMetadata):
1151 """
1152 Catégorie utilisée dans la gestion des Postes.
1153 Catégorie supérieure à TypePoste.
1154 """
1155 nom = models.CharField(max_length=255)
1156
1157 class Meta:
1158 ordering = ('nom',)
1159 verbose_name = u"catégorie d'emploi"
1160 verbose_name_plural = u"catégories d'emploi"
1161
1162 def __unicode__(self):
1163 return self.nom
1164
1165
1166 class FamilleProfessionnelle(models.Model):
1167 """
1168 Famille professionnelle d'un poste.
1169 """
1170 nom = models.CharField(max_length=100)
1171
1172 class Meta:
1173 ordering = ('nom',)
1174 verbose_name = u'famille professionnelle'
1175 verbose_name_plural = u'familles professionnelles'
1176
1177 def __unicode__(self):
1178 return self.nom
1179
1180
1181 class TypePoste(AUFMetadata):
1182 """
1183 Catégorie de Poste.
1184 """
1185 nom = models.CharField(max_length=255)
1186 nom_feminin = models.CharField(u"nom féminin", max_length=255)
1187 is_responsable = models.BooleanField(
1188 u"poste de responsabilité", default=False
1189 )
1190 categorie_emploi = models.ForeignKey(
1191 CategorieEmploi, db_column='categorie_emploi', related_name='+',
1192 verbose_name=u"catégorie d'emploi"
1193 )
1194 famille_professionnelle = models.ForeignKey(
1195 FamilleProfessionnelle, related_name='types_de_poste',
1196 verbose_name=u"famille professionnelle", blank=True, null=True
1197 )
1198
1199 class Meta:
1200 ordering = ['nom']
1201 verbose_name = u"Type de poste"
1202 verbose_name_plural = u"Types de poste"
1203
1204 def __unicode__(self):
1205 return u'%s' % (self.nom)
1206
1207 TYPE_PAIEMENT_CHOICES = (
1208 (u'Régulier', u'Régulier'),
1209 (u'Ponctuel', u'Ponctuel'),
1210 )
1211
1212 NATURE_REMUNERATION_CHOICES = (
1213 (u'Accessoire', u'Accessoire'),
1214 (u'Charges', u'Charges'),
1215 (u'Indemnité', u'Indemnité'),
1216 (u'RAS', u'Rémunération autre source'),
1217 (u'Traitement', u'Traitement'),
1218 )
1219
1220
1221 class TypeRemuneration(AUFMetadata):
1222 """
1223 Catégorie de Remuneration.
1224 """
1225 objects = TypeRemunerationManager()
1226
1227 nom = models.CharField(max_length=255)
1228 type_paiement = models.CharField(
1229 u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
1230 )
1231 nature_remuneration = models.CharField(
1232 u"nature de la rémunération", max_length=30,
1233 choices=NATURE_REMUNERATION_CHOICES
1234 )
1235 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1236
1237 class Meta:
1238 ordering = ['nom']
1239 verbose_name = u"Type de rémunération"
1240 verbose_name_plural = u"Types de rémunération"
1241
1242 def __unicode__(self):
1243 if self.archive:
1244 archive = u"(archivé)"
1245 else:
1246 archive = ""
1247 return u'%s %s' % (self.nom, archive)
1248
1249
1250 class TypeRevalorisation(AUFMetadata):
1251 """
1252 Justification du changement de la Remuneration.
1253 (Actuellement utilisé dans aucun traitement informatique.)
1254 """
1255 nom = models.CharField(max_length=255)
1256
1257 class Meta:
1258 ordering = ['nom']
1259 verbose_name = u"Type de revalorisation"
1260 verbose_name_plural = u"Types de revalorisation"
1261
1262 def __unicode__(self):
1263 return u'%s' % (self.nom)
1264
1265
1266 class Service(AUFMetadata):
1267 """
1268 Unité administrative où les Postes sont rattachés.
1269 """
1270 objects = ServiceManager()
1271
1272 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1273 nom = models.CharField(max_length=255)
1274
1275 class Meta:
1276 ordering = ['nom']
1277 verbose_name = u"Service"
1278 verbose_name_plural = u"Services"
1279
1280 def __unicode__(self):
1281 if self.archive:
1282 archive = u"(archivé)"
1283 else:
1284 archive = ""
1285 return u'%s %s' % (self.nom, archive)
1286
1287
1288 TYPE_ORGANISME_CHOICES = (
1289 ('MAD', 'Mise à disposition'),
1290 ('DET', 'Détachement'),
1291 )
1292
1293
1294 class OrganismeBstg(AUFMetadata):
1295 """
1296 Organisation d'où provient un Employe mis à disposition (MAD) de
1297 ou détaché (DET) à l'AUF à titre gratuit.
1298
1299 (BSTG = bien et service à titre gratuit.)
1300 """
1301 nom = models.CharField(max_length=255)
1302 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
1303 pays = models.ForeignKey(ref.Pays, to_field='code',
1304 db_column='pays',
1305 related_name='organismes_bstg',
1306 null=True, blank=True)
1307
1308 class Meta:
1309 ordering = ['type', 'nom']
1310 verbose_name = u"Organisme BSTG"
1311 verbose_name_plural = u"Organismes BSTG"
1312
1313 def __unicode__(self):
1314 return u'%s (%s)' % (self.nom, self.get_type_display())
1315
1316 prefix_implantation = "pays__region"
1317
1318 def get_regions(self):
1319 return [self.pays.region]
1320
1321
1322 class Statut(AUFMetadata):
1323 """
1324 Statut de l'Employe dans le cadre d'un Dossier particulier.
1325 """
1326 # Identification
1327 code = models.CharField(
1328 max_length=25, unique=True,
1329 help_text=(
1330 u"Saisir un code court mais lisible pour ce statut : "
1331 u"le code est utilisé pour associer les statuts aux autres "
1332 u"données tout en demeurant plus lisible qu'un identifiant "
1333 u"numérique."
1334 )
1335 )
1336 nom = models.CharField(max_length=255)
1337
1338 class Meta:
1339 ordering = ['code']
1340 verbose_name = u"Statut d'employé"
1341 verbose_name_plural = u"Statuts d'employé"
1342
1343 def __unicode__(self):
1344 return u'%s : %s' % (self.code, self.nom)
1345
1346
1347 TYPE_CLASSEMENT_CHOICES = (
1348 ('S', 'S -Soutien'),
1349 ('T', 'T - Technicien'),
1350 ('P', 'P - Professionel'),
1351 ('C', 'C - Cadre'),
1352 ('D', 'D - Direction'),
1353 ('SO', 'SO - Sans objet [expatriés]'),
1354 ('HG', 'HG - Hors grille [direction]'),
1355 )
1356
1357
1358 class ClassementManager(models.Manager):
1359 """
1360 Ordonner les spcéfiquement les classements.
1361 """
1362 def get_query_set(self):
1363 qs = super(self.__class__, self).get_query_set()
1364 qs = qs.extra(select={
1365 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1366 })
1367 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
1368 return qs.all()
1369
1370
1371 class Classement_(AUFMetadata):
1372 """
1373 Éléments de classement de la
1374 "Grille générique de classement hiérarchique".
1375
1376 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1377 classement dans la grille. Le classement donne le coefficient utilisé dans:
1378
1379 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1380 """
1381 objects = ClassementManager()
1382
1383 # Identification
1384 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1385 echelon = models.IntegerField(u"échelon", blank=True, default=0)
1386 degre = models.IntegerField(u"degré", blank=True, default=0)
1387 coefficient = models.FloatField(u"coefficient", default=0, null=True)
1388
1389 # Méta
1390 # annee # au lieu de date_debut et date_fin
1391 commentaire = models.TextField(null=True, blank=True)
1392
1393 class Meta:
1394 abstract = True
1395 ordering = ['type', 'echelon', 'degre', 'coefficient']
1396 verbose_name = u"Classement"
1397 verbose_name_plural = u"Classements"
1398
1399 def __unicode__(self):
1400 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
1401
1402
1403 class Classement(Classement_):
1404 __doc__ = Classement_.__doc__
1405
1406
1407 class TauxChange_(AUFMetadata):
1408 """
1409 Taux de change de la devise vers l'euro (EUR)
1410 pour chaque année budgétaire.
1411 """
1412 # Identification
1413 devise = models.ForeignKey('Devise', db_column='devise')
1414 annee = models.IntegerField(u"année")
1415 taux = models.FloatField(u"taux vers l'euro")
1416
1417 class Meta:
1418 abstract = True
1419 ordering = ['-annee', 'devise__code']
1420 verbose_name = u"Taux de change"
1421 verbose_name_plural = u"Taux de change"
1422
1423 def __unicode__(self):
1424 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1425
1426
1427 class TauxChange(TauxChange_):
1428 __doc__ = TauxChange_.__doc__
1429
1430
1431 class ValeurPointManager(NoDeleteManager):
1432
1433 def get_query_set(self):
1434 return super(ValeurPointManager, self).get_query_set() \
1435 .select_related('devise', 'implantation')
1436
1437
1438 class ValeurPoint_(AUFMetadata):
1439 """
1440 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1441 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1442 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1443
1444 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1445 """
1446
1447 actuelles = ValeurPointManager()
1448
1449 valeur = models.FloatField(null=True)
1450 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
1451 implantation = models.ForeignKey(ref.Implantation,
1452 db_column='implantation',
1453 related_name='%(app_label)s_valeur_point')
1454 # Méta
1455 annee = models.IntegerField()
1456
1457 class Meta:
1458 ordering = ['-annee', 'implantation__nom']
1459 abstract = True
1460 verbose_name = u"Valeur du point"
1461 verbose_name_plural = u"Valeurs du point"
1462
1463 def __unicode__(self):
1464 return u'%s %s %s [%s] %s' % (
1465 self.devise.code, self.annee, self.valeur,
1466 self.implantation.nom_court, self.devise.nom
1467 )
1468
1469
1470 class ValeurPoint(ValeurPoint_):
1471 __doc__ = ValeurPoint_.__doc__
1472
1473
1474 class Devise(AUFMetadata):
1475 """
1476 Devise monétaire.
1477 """
1478 objects = DeviseManager()
1479
1480 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1481 code = models.CharField(max_length=10, unique=True)
1482 nom = models.CharField(max_length=255)
1483
1484 class Meta:
1485 ordering = ['code']
1486 verbose_name = u"Devise"
1487 verbose_name_plural = u"Devises"
1488
1489 def __unicode__(self):
1490 return u'%s - %s' % (self.code, self.nom)
1491
1492
1493 class TypeContrat(AUFMetadata):
1494 """
1495 Type de contrat.
1496 """
1497 nom = models.CharField(max_length=255)
1498 nom_long = models.CharField(max_length=255)
1499
1500 class Meta:
1501 ordering = ['nom']
1502 verbose_name = u"Type de contrat"
1503 verbose_name_plural = u"Types de contrat"
1504
1505 def __unicode__(self):
1506 return u'%s' % (self.nom)
1507
1508
1509 ### AUTRES
1510
1511 class ResponsableImplantationProxy(ref.Implantation):
1512
1513 class Meta:
1514 managed = False
1515 proxy = True
1516 verbose_name = u"Responsable d'implantation"
1517 verbose_name_plural = u"Responsables d'implantation"
1518
1519
1520 class ResponsableImplantation(models.Model):
1521 """
1522 Le responsable d'une implantation.
1523 Anciennement géré sur le Dossier du responsable.
1524 """
1525 employe = models.ForeignKey(
1526 'Employe', db_column='employe', related_name='+', null=True,
1527 blank=True
1528 )
1529 implantation = models.OneToOneField(
1530 "ResponsableImplantationProxy", db_column='implantation',
1531 related_name='responsable', unique=True
1532 )
1533
1534 def __unicode__(self):
1535 return u'%s : %s' % (self.implantation, self.employe)
1536
1537 class Meta:
1538 ordering = ['implantation__nom']
1539 verbose_name = "Responsable d'implantation"
1540 verbose_name_plural = "Responsables d'implantation"