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