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