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