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