[#5050] Permettre la restauration d'objets supprimés
[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
b0cf30b8 344 prefix_implantation = "implantation__zone_administrative"
fa1f7426 345
b0cf30b8
EMS
346 def get_zones_administratives(self):
347 return [self.implantation.zone_administrative]
4c53dda4 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 580 params = {KEY_STATUT: STATUT_INACTIF, }
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
c267f20c 586 def dossiers_futurs(self):
6bee05ff 587 params = {KEY_STATUT: STATUT_FUTUR, }
da202402 588 search = RechercheTemporelle(params, Dossier)
6bee05ff 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 594 params = {KEY_STATUT: STATUT_ACTIF, }
da202402 595 search = RechercheTemporelle(params, Dossier)
6bee05ff 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
b0cf30b8
EMS
632 prefix_implantation = \
633 "rh_dossiers__poste__implantation__zone_administrative"
fa1f7426 634
b0cf30b8
EMS
635 def get_zones_administratives(self):
636 return [
637 d.poste.implantation.zone_administrative
638 for d in self.dossiers.all()
639 ]
aff1a4c6 640
d104b0ae 641reversion.register(Employe, format='xml', follow=[
45066657 642 'pieces', 'commentaires', 'ayantdroits'
d104b0ae
EMS
643])
644
aff1a4c6 645
7abc6d45 646class EmployePiece(models.Model):
fa1f7426
EMS
647 """
648 Documents relatifs à un employé.
7abc6d45 649 Ex.: CV...
650 """
fa1f7426
EMS
651 employe = models.ForeignKey(
652 'Employe', db_column='employe', related_name="pieces",
653 verbose_name=u"employé"
654 )
655 nom = models.CharField(max_length=255)
656 fichier = models.FileField(
657 u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive
658 )
7abc6d45 659
6e4600ef 660 class Meta:
661 ordering = ['nom']
f9e54d59
PP
662 verbose_name = u"Employé pièce"
663 verbose_name_plural = u"Employé pièces"
664
6e4600ef 665 def __unicode__(self):
666 return u'%s' % (self.nom)
667
d104b0ae
EMS
668reversion.register(EmployePiece, format='xml')
669
fa1f7426 670
07b40eda 671class EmployeCommentaire(Commentaire):
fa1f7426 672 employe = models.ForeignKey(
d104b0ae 673 'Employe', db_column='employe', related_name='commentaires'
fa1f7426 674 )
9afaa55e 675
b343eb3d
PP
676 class Meta:
677 verbose_name = u"Employé commentaire"
678 verbose_name_plural = u"Employé commentaires"
679
d104b0ae
EMS
680reversion.register(EmployeCommentaire, format='xml')
681
2d4d2fcf 682
e9bbd6ba 683LIEN_PARENTE_CHOICES = (
684 ('Conjoint', 'Conjoint'),
685 ('Conjointe', 'Conjointe'),
686 ('Fille', 'Fille'),
687 ('Fils', 'Fils'),
688)
689
fa1f7426 690
45066657 691class AyantDroit(models.Model):
fa1f7426
EMS
692 """
693 Personne en relation avec un Employe.
6e4600ef 694 """
9afaa55e 695 # Identification
e9bbd6ba 696 nom = models.CharField(max_length=255)
fa1f7426
EMS
697 prenom = models.CharField(u"prénom", max_length=255)
698 nom_affichage = models.CharField(
699 u"nom d'affichage", max_length=255, null=True, blank=True
700 )
701 nationalite = models.ForeignKey(
702 ref.Pays, to_field='code', db_column='nationalite',
703 related_name='ayantdroits_nationalite',
704 verbose_name=u"nationalité", null=True, blank=True
705 )
706 date_naissance = models.DateField(
707 u"Date de naissance", help_text=HELP_TEXT_DATE,
708 validators=[validate_date_passee], null=True, blank=True
709 )
2d4d2fcf 710 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
ca1a7b76 711
9afaa55e 712 # Relation
fa1f7426
EMS
713 employe = models.ForeignKey(
714 'Employe', db_column='employe', related_name='ayantdroits',
715 verbose_name=u"Employé"
716 )
717 lien_parente = models.CharField(
718 u"lien de parenté", max_length=10, choices=LIEN_PARENTE_CHOICES,
719 null=True, blank=True
720 )
6e4600ef 721
722 class Meta:
a2c3ad52 723 ordering = ['nom', ]
c1195471
OL
724 verbose_name = u"Ayant droit"
725 verbose_name_plural = u"Ayants droit"
ca1a7b76 726
6e4600ef 727 def __unicode__(self):
2de29065 728 return u'%s %s' % (self.nom.upper(), self.prenom, )
83b7692b 729
b0cf30b8
EMS
730 prefix_implantation = \
731 "employe__dossiers__poste__implantation__zone_administrative"
fa1f7426 732
b0cf30b8
EMS
733 def get_zones_administratives(self):
734 return [
735 d.poste.implantation.zone_administrative
736 for d in self.employe.dossiers.all()
737 ]
aff1a4c6 738
d104b0ae
EMS
739reversion.register(AyantDroit, format='xml', follow=['commentaires'])
740
aff1a4c6 741
07b40eda 742class AyantDroitCommentaire(Commentaire):
fa1f7426 743 ayant_droit = models.ForeignKey(
d104b0ae 744 'AyantDroit', db_column='ayant_droit', related_name='commentaires'
fa1f7426 745 )
83b7692b 746
d104b0ae
EMS
747reversion.register(AyantDroitCommentaire, format='xml')
748
2d4d2fcf 749
83b7692b 750### DOSSIER
751
752STATUT_RESIDENCE_CHOICES = (
753 ('local', 'Local'),
754 ('expat', 'Expatrié'),
755)
756
757COMPTE_COMPTA_CHOICES = (
758 ('coda', 'CODA'),
759 ('scs', 'SCS'),
760 ('aucun', 'Aucun'),
761)
762
fa1f7426 763
52f4c1e7 764class Dossier_(DateActiviteMixin, models.Model, DevisableMixin,):
fa1f7426
EMS
765 """
766 Le Dossier regroupe les informations relatives à l'occupation
6e4600ef 767 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
768 par un Employe.
ca1a7b76 769
6e4600ef 770 Plusieurs Contrats peuvent être associés au Dossier.
771 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
772 lequel aucun Dossier n'existe est un poste vacant.
773 """
3f5cbabe
OL
774
775 objects = DossierManager()
776
63e17dff 777 # TODO: OneToOne ??
eb6bf568 778 statut = models.ForeignKey('Statut', related_name='+', null=True)
fa1f7426
EMS
779 organisme_bstg = models.ForeignKey(
780 'OrganismeBstg', db_column='organisme_bstg', related_name='+',
781 verbose_name=u"organisme",
782 help_text=(
783 u"Si détaché (DET) ou mis à disposition (MAD), "
784 u"préciser l'organisme."
785 ), null=True, blank=True
786 )
ca1a7b76 787
83b7692b 788 # Recrutement
2d4d2fcf 789 remplacement = models.BooleanField(default=False)
fa1f7426
EMS
790 remplacement_de = models.ForeignKey(
791 'self', related_name='+', help_text=u"Taper le nom de l'employé",
792 null=True, blank=True
793 )
794 statut_residence = models.CharField(
795 u"statut", max_length=10, default='local', null=True,
796 choices=STATUT_RESIDENCE_CHOICES
797 )
ca1a7b76 798
83b7692b 799 # Rémunération
fa1f7426
EMS
800 classement = models.ForeignKey(
801 'Classement', db_column='classement', related_name='+', null=True,
802 blank=True
803 )
804 regime_travail = models.DecimalField(
805 u"régime de travail", max_digits=12, null=True, decimal_places=2,
806 default=REGIME_TRAVAIL_DEFAULT, help_text="% du temps complet"
807 )
808 regime_travail_nb_heure_semaine = models.DecimalField(
809 u"nb. heures par semaine", max_digits=12,
810 decimal_places=2, null=True,
811 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
812 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
813 )
7abc6d45 814
815 # Occupation du Poste par cet Employe (anciennement "mandat")
8bb6f549
EMS
816 date_debut = models.DateField(
817 u"date de début d'occupation de poste", db_index=True
818 )
fa1f7426 819 date_fin = models.DateField(
8bb6f549
EMS
820 u"Date de fin d'occupation de poste", null=True, blank=True,
821 db_index=True
fa1f7426 822 )
ca1a7b76 823
2d4d2fcf 824 # Comptes
825 # TODO?
ca1a7b76 826
6e4600ef 827 class Meta:
37868f0b 828 abstract = True
49449367 829 ordering = ['employe__nom', ]
3f5f3898 830 verbose_name = u"Dossier"
8c1ae2b3 831 verbose_name_plural = "Dossiers"
ca1a7b76 832
65f9fac8 833 def salaire_theorique(self):
834 annee = date.today().year
835 coeff = self.classement.coefficient
836 implantation = self.poste.implantation
837 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
ca1a7b76 838
65f9fac8 839 montant = coeff * point.valeur
840 devise = point.devise
fa1f7426 841 return {'montant': montant, 'devise': devise}
ca1a7b76 842
83b7692b 843 def __unicode__(self):
8c1ae2b3 844 poste = self.poste.nom
845 if self.employe.genre == 'F':
ca1a7b76 846 poste = self.poste.nom_feminin
8c1ae2b3 847 return u'%s - %s' % (self.employe, poste)
83b7692b 848
b0cf30b8 849 prefix_implantation = "poste__implantation__zone_administrative"
fa1f7426 850
b0cf30b8
EMS
851 def get_zones_administratives(self):
852 return [self.poste.implantation.zone_administrative]
aff1a4c6 853
3ebc0952 854 def remunerations(self):
838bc59d
OL
855 key = "%s_remunerations" % self._meta.app_label
856 remunerations = getattr(self, key)
857 return remunerations.all().order_by('-date_debut')
3ebc0952 858
02e69aa2 859 def remunerations_en_cours(self):
838bc59d
OL
860 q = Q(date_fin__exact=None) | Q(date_fin__gt=datetime.date.today())
861 return self.remunerations().all().filter(q).order_by('date_debut')
02e69aa2 862
09aa8374
OL
863 def get_salaire(self):
864 try:
fa1f7426
EMS
865 return [r for r in self.remunerations().order_by('-date_debut')
866 if r.type_id == 1][0]
09aa8374
OL
867 except:
868 return None
3ebc0952 869
838bc59d
OL
870 def get_salaire_euros(self):
871 tx = self.taux_devise()
872 return (float)(tx) * (float)(self.salaire)
873
874 def get_remunerations_brutes(self):
875 """
876 1 Salaire de base
877 3 Indemnité de base
878 4 Indemnité d'expatriation
879 5 Indemnité pour frais
880 6 Indemnité de logement
881 7 Indemnité de fonction
882 8 Indemnité de responsabilité
883 9 Indemnité de transport
884 10 Indemnité compensatrice
885 11 Indemnité de subsistance
886 12 Indemnité différentielle
887 13 Prime d'installation
888 14 Billet d'avion
889 15 Déménagement
890 16 Indemnité de départ
891 18 Prime de 13ième mois
892 19 Prime d'intérim
893 """
fa1f7426
EMS
894 ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
895 return [r for r in self.remunerations_en_cours().all()
896 if r.type_id in ids]
838bc59d
OL
897
898 def get_charges_salariales(self):
899 """
900 20 Charges salariales ?
901 """
fa1f7426
EMS
902 ids = [20]
903 return [r for r in self.remunerations_en_cours().all()
904 if r.type_id in ids]
838bc59d 905
838bc59d
OL
906 def get_charges_patronales(self):
907 """
908 17 Charges patronales
909 """
fa1f7426
EMS
910 ids = [17]
911 return [r for r in self.remunerations_en_cours().all()
912 if r.type_id in ids]
838bc59d 913
552d0db7
OL
914 def get_remunerations_tierces(self):
915 """
916 2 Salaire MAD
917 """
fa1f7426
EMS
918 return [r for r in self.remunerations_en_cours().all()
919 if r.type_id in (2,)]
552d0db7
OL
920
921 # DEVISE LOCALE
922
923 def get_total_local_charges_salariales(self):
bc17b82c 924 devise = self.poste.get_devise()
552d0db7
OL
925 total = 0.0
926 for r in self.get_charges_salariales():
bc17b82c
OL
927 if r.devise != devise:
928 return None
929 total += float(r.montant)
552d0db7
OL
930 return total
931
932 def get_total_local_charges_patronales(self):
bc17b82c 933 devise = self.poste.get_devise()
552d0db7
OL
934 total = 0.0
935 for r in self.get_charges_patronales():
bc17b82c
OL
936 if r.devise != devise:
937 return None
552d0db7
OL
938 total += float(r.montant)
939 return total
940
941 def get_local_salaire_brut(self):
942 """
943 somme des rémuérations brutes
944 """
945 devise = self.poste.get_devise()
946 total = 0.0
947 for r in self.get_remunerations_brutes():
948 if r.devise != devise:
949 return None
950 total += float(r.montant)
951 return total
952
953 def get_local_salaire_net(self):
954 """
955 salaire brut - charges salariales
956 """
957 devise = self.poste.get_devise()
958 total_charges = 0.0
959 for r in self.get_charges_salariales():
960 if r.devise != devise:
961 return None
962 total_charges += float(r.montant)
963 return self.get_local_salaire_brut() - total_charges
964
965 def get_local_couts_auf(self):
966 """
967 salaire net + charges patronales
968 """
969 devise = self.poste.get_devise()
970 total_charges = 0.0
971 for r in self.get_charges_patronales():
972 if r.devise != devise:
973 return None
974 total_charges += float(r.montant)
975 return self.get_local_salaire_net() + total_charges
976
977 def get_total_local_remunerations_tierces(self):
978 devise = self.poste.get_devise()
979 total = 0.0
980 for r in self.get_remunerations_tierces():
981 if r.devise != devise:
982 return None
983 total += float(r.montant)
984 return total
985
986 # DEVISE EURO
987
988 def get_total_charges_salariales(self):
989 total = 0.0
990 for r in self.get_charges_salariales():
991 total += r.montant_euros()
992 return total
993
838bc59d
OL
994 def get_total_charges_patronales(self):
995 total = 0.0
996 for r in self.get_charges_patronales():
997 total += r.montant_euros()
998 return total
999
1000 def get_salaire_brut(self):
1001 """
1002 somme des rémuérations brutes
1003 """
1004 total = 0.0
1005 for r in self.get_remunerations_brutes():
1006 total += r.montant_euros()
1007 return total
1008
1009 def get_salaire_net(self):
1010 """
1011 salaire brut - charges salariales
1012 """
1013 total_charges = 0.0
1014 for r in self.get_charges_salariales():
1015 total_charges += r.montant_euros()
1016 return self.get_salaire_brut() - total_charges
1017
1018 def get_couts_auf(self):
1019 """
1020 salaire net + charges patronales
1021 """
1022 total_charges = 0.0
1023 for r in self.get_charges_patronales():
1024 total_charges += r.montant_euros()
1025 return self.get_salaire_net() + total_charges
1026
838bc59d
OL
1027 def get_total_remunerations_tierces(self):
1028 total = 0.0
1029 for r in self.get_remunerations_tierces():
1030 total += r.montant_euros()
1031 return total
1032
5db1c5a3
DB
1033 def premier_contrat(self):
1034 """contrat avec plus petite date de début"""
1035 try:
db265492
EMS
1036 contrat = self.rh_contrats.exclude(date_debut=None) \
1037 .order_by('date_debut')[0]
5db1c5a3
DB
1038 except IndexError, Contrat.DoesNotExist:
1039 contrat = None
1040 return contrat
db265492 1041
5db1c5a3
DB
1042 def dernier_contrat(self):
1043 """contrat avec plus grande date de fin"""
1044 try:
db265492
EMS
1045 contrat = self.rh_contrats.exclude(date_debut=None) \
1046 .order_by('-date_debut')[0]
5db1c5a3
DB
1047 except IndexError, Contrat.DoesNotExist:
1048 contrat = None
1049 return contrat
1050
bfb5e43e
EMS
1051 def actif(self):
1052 today = date.today()
1053 return (self.date_debut is None or self.date_debut <= today) \
1054 and (self.date_fin is None or self.date_fin >= today) \
1055 and not (self.date_fin is None and self.date_debut is None)
1056
22343fe7 1057
37868f0b
NC
1058class Dossier(Dossier_):
1059 __doc__ = Dossier_.__doc__
4ba84959
EMS
1060 poste = models.ForeignKey(
1061 Poste, db_column='poste', related_name='rh_dossiers',
0b0545bd 1062 help_text=u"Taper le nom du poste ou du type de poste",
4ba84959 1063 )
fa1f7426
EMS
1064 employe = models.ForeignKey(
1065 'Employe', db_column='employe',
1066 help_text=u"Taper le nom de l'employé",
4ba84959
EMS
1067 related_name='rh_dossiers', verbose_name=u"employé"
1068 )
fa1f7426 1069 principal = models.BooleanField(
c1f5d83c 1070 u"dossier principal", default=True,
fa1f7426
EMS
1071 help_text=(
1072 u"Ce dossier est pour le principal poste occupé par l'employé"
1073 )
1074 )
37868f0b 1075
d104b0ae
EMS
1076reversion.register(Dossier, format='xml', follow=[
1077 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
1078 'rh_contrats', 'commentaires'
1079])
1080
37868f0b 1081
fc917340 1082class DossierPiece_(models.Model):
fa1f7426
EMS
1083 """
1084 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
7abc6d45 1085 Ex.: Lettre de motivation.
1086 """
fa1f7426
EMS
1087 nom = models.CharField(max_length=255)
1088 fichier = models.FileField(
1089 upload_to=dossier_piece_dispatch, storage=storage_prive
1090 )
83b7692b 1091
6e4600ef 1092 class Meta:
fc917340 1093 abstract = True
6e4600ef 1094 ordering = ['nom']
ca1a7b76 1095
6e4600ef 1096 def __unicode__(self):
1097 return u'%s' % (self.nom)
1098
fa1f7426 1099
fc917340 1100class DossierPiece(DossierPiece_):
fa1f7426 1101 dossier = models.ForeignKey(
4ba84959 1102 Dossier, db_column='dossier', related_name='rh_dossierpieces'
fa1f7426
EMS
1103 )
1104
d104b0ae 1105reversion.register(DossierPiece, format='xml')
fc917340 1106
4ba84959
EMS
1107class DossierCommentaire(Commentaire):
1108 dossier = models.ForeignKey(
1109 Dossier, db_column='dossier', related_name='commentaires'
1110 )
fc917340 1111
d104b0ae
EMS
1112reversion.register(DossierCommentaire, format='xml')
1113
fa1f7426 1114
e84c8ef1 1115class DossierComparaison_(models.Model, DevisableMixin):
1d0f4eef
OL
1116 """
1117 Photo d'une comparaison salariale au moment de l'embauche.
1118 """
16b1454e
OL
1119 objects = DossierComparaisonManager()
1120
fa1f7426
EMS
1121 implantation = models.ForeignKey(
1122 ref.Implantation, related_name="+", null=True, blank=True
1123 )
1d0f4eef
OL
1124 poste = models.CharField(max_length=255, null=True, blank=True)
1125 personne = models.CharField(max_length=255, null=True, blank=True)
1126 montant = models.IntegerField(null=True)
fa1f7426
EMS
1127 devise = models.ForeignKey(
1128 'Devise', related_name='+', null=True, blank=True
1129 )
1d0f4eef 1130
fc917340
OL
1131 class Meta:
1132 abstract = True
1133
3b14230d
OL
1134 def __unicode__(self):
1135 return "%s (%s)" % (self.poste, self.personne)
1136
1d0f4eef 1137
fc917340 1138class DossierComparaison(DossierComparaison_):
4ba84959
EMS
1139 dossier = models.ForeignKey(
1140 Dossier, related_name='rh_comparaisons'
1141 )
2d4d2fcf 1142
d104b0ae
EMS
1143reversion.register(DossierComparaison, format='xml')
1144
fa1f7426 1145
07b40eda 1146### RÉMUNÉRATION
ca1a7b76 1147
45066657 1148class RemunerationMixin(models.Model):
fa1f7426 1149
9afaa55e 1150 # Identification
fa1f7426
EMS
1151 type = models.ForeignKey(
1152 'TypeRemuneration', db_column='type', related_name='+',
1153 verbose_name=u"type de rémunération"
1154 )
1155 type_revalorisation = models.ForeignKey(
1156 'TypeRevalorisation', db_column='type_revalorisation',
1157 related_name='+', verbose_name=u"type de revalorisation",
1158 null=True, blank=True
1159 )
1160 montant = models.DecimalField(
d104b0ae 1161 null=True, blank=True, max_digits=12, decimal_places=2
fa1f7426
EMS
1162 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1163 devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
1164
2d4d2fcf 1165 # commentaire = precision
1166 commentaire = models.CharField(max_length=255, null=True, blank=True)
fa1f7426 1167
2d4d2fcf 1168 # date_debut = anciennement date_effectif
8bb6f549
EMS
1169 date_debut = models.DateField(
1170 u"date de début", null=True, blank=True, db_index=True
1171 )
1172 date_fin = models.DateField(
1173 u"date de fin", null=True, blank=True, db_index=True
1174 )
ca1a7b76
EMS
1175
1176 class Meta:
2d4d2fcf 1177 abstract = True
6e4600ef 1178 ordering = ['type__nom', '-date_fin']
ca1a7b76 1179
6e4600ef 1180 def __unicode__(self):
1181 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
ca1a7b76 1182
fa1f7426 1183
e84c8ef1 1184class Remuneration_(RemunerationMixin, DevisableMixin):
fa1f7426
EMS
1185 """
1186 Structure de rémunération (données budgétaires) en situation normale
ca1a7b76
EMS
1187 pour un Dossier. Si un Evenement existe, utiliser la structure de
1188 rémunération EvenementRemuneration de cet événement.
2d4d2fcf 1189 """
c3550a05 1190 objects = RemunerationManager()
83b7692b 1191
1192 def montant_mois(self):
1193 return round(self.montant / 12, 2)
1194
626beb4d 1195 def montant_avec_regime(self):
fa1f7426 1196 return round(self.montant * (self.dossier.regime_travail / 100), 2)
626beb4d 1197
83b7692b 1198 def montant_euro_mois(self):
e84c8ef1 1199 return round(self.montant_euros() / 12, 2)
ca1a7b76 1200
9afaa55e 1201 def __unicode__(self):
1202 try:
1203 devise = self.devise.code
1204 except:
1205 devise = "???"
1206 return "%s %s" % (self.montant, devise)
83b7692b 1207
6e7c919b
NC
1208 class Meta:
1209 abstract = True
c1195471
OL
1210 verbose_name = u"Rémunération"
1211 verbose_name_plural = u"Rémunérations"
6e7c919b
NC
1212
1213
1214class Remuneration(Remuneration_):
4ba84959
EMS
1215 dossier = models.ForeignKey(
1216 Dossier, db_column='dossier', related_name='rh_remunerations'
1217 )
6e7c919b 1218
d104b0ae
EMS
1219reversion.register(Remuneration, format='xml')
1220
2d4d2fcf 1221
1222### CONTRATS
c41b7fcc 1223
45066657 1224class Contrat_(models.Model):
fa1f7426
EMS
1225 """
1226 Document juridique qui encadre la relation de travail d'un Employe
ca1a7b76 1227 pour un Poste particulier. Pour un Dossier (qui documente cette
2d4d2fcf 1228 relation de travail) plusieurs contrats peuvent être associés.
1229 """
c41b7fcc 1230 objects = ContratManager()
fa1f7426
EMS
1231 type_contrat = models.ForeignKey(
1232 'TypeContrat', db_column='type_contrat',
1233 verbose_name=u'type de contrat', related_name='+'
1234 )
8bb6f549
EMS
1235 date_debut = models.DateField(
1236 u"date de début", db_index=True
1237 )
1238 date_fin = models.DateField(
1239 u"date de fin", null=True, blank=True, db_index=True
1240 )
fa1f7426
EMS
1241 fichier = models.FileField(
1242 upload_to=contrat_dispatch, storage=storage_prive, null=True,
1243 blank=True
1244 )
6e4600ef 1245
1246 class Meta:
fc917340 1247 abstract = True
a2c3ad52 1248 ordering = ['dossier__employe__nom']
c1195471
OL
1249 verbose_name = u"Contrat"
1250 verbose_name_plural = u"Contrats"
ca1a7b76 1251
6e4600ef 1252 def __unicode__(self):
8c1ae2b3 1253 return u'%s - %s' % (self.dossier, self.id)
fc917340 1254
fa1f7426 1255
fc917340 1256class Contrat(Contrat_):
4ba84959
EMS
1257 dossier = models.ForeignKey(
1258 Dossier, db_column='dossier', related_name='rh_contrats'
1259 )
f31ddfa0 1260
d104b0ae
EMS
1261reversion.register(Contrat, format='xml')
1262
83b7692b 1263
ca1a7b76 1264### RÉFÉRENCES RH
83b7692b 1265
45066657 1266class CategorieEmploi(models.Model):
fa1f7426
EMS
1267 """
1268 Catégorie utilisée dans la gestion des Postes.
6e4600ef 1269 Catégorie supérieure à TypePoste.
1270 """
e9bbd6ba 1271 nom = models.CharField(max_length=255)
ca1a7b76 1272
8c1ae2b3 1273 class Meta:
321fe481 1274 ordering = ('nom',)
7bf28694
EMS
1275 verbose_name = u"catégorie d'emploi"
1276 verbose_name_plural = u"catégories d'emploi"
ca1a7b76 1277
6e4600ef 1278 def __unicode__(self):
321fe481
EMS
1279 return self.nom
1280
d104b0ae
EMS
1281reversion.register(CategorieEmploi, format='xml')
1282
321fe481
EMS
1283
1284class FamilleProfessionnelle(models.Model):
1285 """
1286 Famille professionnelle d'un poste.
1287 """
1288 nom = models.CharField(max_length=100)
1289
1290 class Meta:
1291 ordering = ('nom',)
1292 verbose_name = u'famille professionnelle'
1293 verbose_name_plural = u'familles professionnelles'
1294
1295 def __unicode__(self):
1296 return self.nom
e9bbd6ba 1297
d104b0ae
EMS
1298reversion.register(FamilleProfessionnelle, format='xml')
1299
fa1f7426 1300
45066657 1301class TypePoste(models.Model):
fa1f7426
EMS
1302 """
1303 Catégorie de Poste.
6e4600ef 1304 """
e9bbd6ba 1305 nom = models.CharField(max_length=255)
fa1f7426
EMS
1306 nom_feminin = models.CharField(u"nom féminin", max_length=255)
1307 is_responsable = models.BooleanField(
1308 u"poste de responsabilité", default=False
1309 )
7bf28694
EMS
1310 categorie_emploi = models.ForeignKey(
1311 CategorieEmploi, db_column='categorie_emploi', related_name='+',
1312 verbose_name=u"catégorie d'emploi"
fa1f7426 1313 )
321fe481
EMS
1314 famille_professionnelle = models.ForeignKey(
1315 FamilleProfessionnelle, related_name='types_de_poste',
1316 verbose_name=u"famille professionnelle", blank=True, null=True
1317 )
e9bbd6ba 1318
6e4600ef 1319 class Meta:
1320 ordering = ['nom']
c1195471
OL
1321 verbose_name = u"Type de poste"
1322 verbose_name_plural = u"Types de poste"
ca1a7b76 1323
e9bbd6ba 1324 def __unicode__(self):
6e4600ef 1325 return u'%s' % (self.nom)
e9bbd6ba 1326
d104b0ae
EMS
1327reversion.register(TypePoste, format='xml')
1328
1329
e9bbd6ba 1330TYPE_PAIEMENT_CHOICES = (
a3e3bde0
JPC
1331 (u'Régulier', u'Régulier'),
1332 (u'Ponctuel', u'Ponctuel'),
e9bbd6ba 1333)
1334
1335NATURE_REMUNERATION_CHOICES = (
a3e3bde0
JPC
1336 (u'Accessoire', u'Accessoire'),
1337 (u'Charges', u'Charges'),
1338 (u'Indemnité', u'Indemnité'),
1339 (u'RAS', u'Rémunération autre source'),
1340 (u'Traitement', u'Traitement'),
e9bbd6ba 1341)
1342
fa1f7426 1343
7013d234 1344class TypeRemuneration(Archivable):
fa1f7426
EMS
1345 """
1346 Catégorie de Remuneration.
6e4600ef 1347 """
7ba822a6
OL
1348 objects = TypeRemunerationManager()
1349
e9bbd6ba 1350 nom = models.CharField(max_length=255)
fa1f7426
EMS
1351 type_paiement = models.CharField(
1352 u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
1353 )
1354 nature_remuneration = models.CharField(
1355 u"nature de la rémunération", max_length=30,
1356 choices=NATURE_REMUNERATION_CHOICES
1357 )
ca1a7b76 1358
8c1ae2b3 1359 class Meta:
1360 ordering = ['nom']
c1195471
OL
1361 verbose_name = u"Type de rémunération"
1362 verbose_name_plural = u"Types de rémunération"
9afaa55e 1363
1364 def __unicode__(self):
7013d234 1365 return self.nom
ca1a7b76 1366
d104b0ae
EMS
1367reversion.register(TypeRemuneration, format='xml')
1368
fa1f7426 1369
45066657 1370class TypeRevalorisation(models.Model):
fa1f7426
EMS
1371 """
1372 Justification du changement de la Remuneration.
6e4600ef 1373 (Actuellement utilisé dans aucun traitement informatique.)
7abc6d45 1374 """
e9bbd6ba 1375 nom = models.CharField(max_length=255)
ca1a7b76 1376
8c1ae2b3 1377 class Meta:
1378 ordering = ['nom']
c1195471
OL
1379 verbose_name = u"Type de revalorisation"
1380 verbose_name_plural = u"Types de revalorisation"
e9bbd6ba 1381
1382 def __unicode__(self):
6e4600ef 1383 return u'%s' % (self.nom)
ca1a7b76 1384
d104b0ae
EMS
1385reversion.register(TypeRevalorisation, format='xml')
1386
fa1f7426 1387
7013d234 1388class Service(Archivable):
fa1f7426
EMS
1389 """
1390 Unité administrative où les Postes sont rattachés.
6e4600ef 1391 """
1392 nom = models.CharField(max_length=255)
ca1a7b76 1393
9afaa55e 1394 class Meta:
1395 ordering = ['nom']
7013d234
EMS
1396 verbose_name = u"service"
1397 verbose_name_plural = u"services"
e9bbd6ba 1398
6e4600ef 1399 def __unicode__(self):
7013d234 1400 return self.nom
6e4600ef 1401
d104b0ae
EMS
1402reversion.register(Service, format='xml')
1403
e9bbd6ba 1404
1405TYPE_ORGANISME_CHOICES = (
1406 ('MAD', 'Mise à disposition'),
1407 ('DET', 'Détachement'),
1408)
1409
fa1f7426 1410
45066657 1411class OrganismeBstg(models.Model):
fa1f7426
EMS
1412 """
1413 Organisation d'où provient un Employe mis à disposition (MAD) de
6e4600ef 1414 ou détaché (DET) à l'AUF à titre gratuit.
ca1a7b76 1415
6e4600ef 1416 (BSTG = bien et service à titre gratuit.)
1417 """
e9bbd6ba 1418 nom = models.CharField(max_length=255)
1419 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
ca1a7b76 1420 pays = models.ForeignKey(ref.Pays, to_field='code',
6e4600ef 1421 db_column='pays',
1422 related_name='organismes_bstg',
1423 null=True, blank=True)
9afaa55e 1424
1425 class Meta:
1426 ordering = ['type', 'nom']
c1195471
OL
1427 verbose_name = u"Organisme BSTG"
1428 verbose_name_plural = u"Organismes BSTG"
9afaa55e 1429
6e4600ef 1430 def __unicode__(self):
8c1ae2b3 1431 return u'%s (%s)' % (self.nom, self.get_type_display())
83b7692b 1432
d104b0ae
EMS
1433reversion.register(OrganismeBstg, format='xml')
1434
aff1a4c6 1435
45066657 1436class Statut(models.Model):
fa1f7426
EMS
1437 """
1438 Statut de l'Employe dans le cadre d'un Dossier particulier.
6e4600ef 1439 """
9afaa55e 1440 # Identification
fa1f7426
EMS
1441 code = models.CharField(
1442 max_length=25, unique=True,
1443 help_text=(
1444 u"Saisir un code court mais lisible pour ce statut : "
1445 u"le code est utilisé pour associer les statuts aux autres "
1446 u"données tout en demeurant plus lisible qu'un identifiant "
1447 u"numérique."
1448 )
1449 )
e9bbd6ba 1450 nom = models.CharField(max_length=255)
e9bbd6ba 1451
6e4600ef 1452 class Meta:
1453 ordering = ['code']
c1195471
OL
1454 verbose_name = u"Statut d'employé"
1455 verbose_name_plural = u"Statuts d'employé"
ca1a7b76 1456
9afaa55e 1457 def __unicode__(self):
1458 return u'%s : %s' % (self.code, self.nom)
1459
d104b0ae
EMS
1460reversion.register(Statut, format='xml')
1461
83b7692b 1462
e9bbd6ba 1463TYPE_CLASSEMENT_CHOICES = (
6e4600ef 1464 ('S', 'S -Soutien'),
1465 ('T', 'T - Technicien'),
1466 ('P', 'P - Professionel'),
1467 ('C', 'C - Cadre'),
1468 ('D', 'D - Direction'),
1469 ('SO', 'SO - Sans objet [expatriés]'),
1470 ('HG', 'HG - Hors grille [direction]'),
e9bbd6ba 1471)
83b7692b 1472
fa1f7426 1473
952ecb37
OL
1474class ClassementManager(models.Manager):
1475 """
1476 Ordonner les spcéfiquement les classements.
1477 """
1478 def get_query_set(self):
1479 qs = super(self.__class__, self).get_query_set()
fa1f7426
EMS
1480 qs = qs.extra(select={
1481 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1482 })
6559f73b 1483 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
952ecb37
OL
1484 return qs.all()
1485
6e7c919b 1486
45066657 1487class Classement_(models.Model):
fa1f7426
EMS
1488 """
1489 Éléments de classement de la
6e4600ef 1490 "Grille générique de classement hiérarchique".
ca1a7b76
EMS
1491
1492 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
6e4600ef 1493 classement dans la grille. Le classement donne le coefficient utilisé dans:
1494
1495 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1496 """
952ecb37
OL
1497 objects = ClassementManager()
1498
9afaa55e 1499 # Identification
e9bbd6ba 1500 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
fa1f7426
EMS
1501 echelon = models.IntegerField(u"échelon", blank=True, default=0)
1502 degre = models.IntegerField(u"degré", blank=True, default=0)
d104b0ae 1503 coefficient = models.FloatField(u"coefficient", blank=True, null=True)
fa1f7426 1504
9afaa55e 1505 # Méta
6e4600ef 1506 # annee # au lieu de date_debut et date_fin
1507 commentaire = models.TextField(null=True, blank=True)
ca1a7b76 1508
6e4600ef 1509 class Meta:
6e7c919b 1510 abstract = True
fa1f7426 1511 ordering = ['type', 'echelon', 'degre', 'coefficient']
c1195471
OL
1512 verbose_name = u"Classement"
1513 verbose_name_plural = u"Classements"
e9bbd6ba 1514
1515 def __unicode__(self):
22343fe7 1516 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
e9bbd6ba 1517
fa1f7426 1518
6e7c919b
NC
1519class Classement(Classement_):
1520 __doc__ = Classement_.__doc__
1521
d104b0ae
EMS
1522reversion.register(Classement, format='xml')
1523
6e7c919b 1524
45066657 1525class TauxChange_(models.Model):
fa1f7426
EMS
1526 """
1527 Taux de change de la devise vers l'euro (EUR)
6e4600ef 1528 pour chaque année budgétaire.
7abc6d45 1529 """
9afaa55e 1530 # Identification
8d3e2fff 1531 devise = models.ForeignKey('Devise', db_column='devise')
fa1f7426
EMS
1532 annee = models.IntegerField(u"année")
1533 taux = models.FloatField(u"taux vers l'euro")
6e7c919b 1534
6e4600ef 1535 class Meta:
6e7c919b 1536 abstract = True
8c1ae2b3 1537 ordering = ['-annee', 'devise__code']
c1195471
OL
1538 verbose_name = u"Taux de change"
1539 verbose_name_plural = u"Taux de change"
8bb6f549 1540 unique_together = ('devise', 'annee')
ca1a7b76 1541
6e4600ef 1542 def __unicode__(self):
8c1ae2b3 1543 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
e9bbd6ba 1544
6e7c919b
NC
1545
1546class TauxChange(TauxChange_):
1547 __doc__ = TauxChange_.__doc__
1548
d104b0ae
EMS
1549reversion.register(TauxChange, format='xml')
1550
fa1f7426 1551
45066657 1552class ValeurPointManager(models.Manager):
105dd778 1553
701f3bea 1554 def get_query_set(self):
fa1f7426
EMS
1555 return super(ValeurPointManager, self).get_query_set() \
1556 .select_related('devise', 'implantation')
701f3bea 1557
6e7c919b 1558
45066657 1559class ValeurPoint_(models.Model):
fa1f7426
EMS
1560 """
1561 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
ca1a7b76 1562 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
8c1ae2b3 1563 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
6e4600ef 1564
1565 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1566 """
ca1a7b76 1567
45066657 1568 objects = models.Manager()
09aa8374 1569 actuelles = ValeurPointManager()
701f3bea 1570
8277a35b 1571 valeur = models.FloatField(null=True)
f614ca5c 1572 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
ca1a7b76 1573 implantation = models.ForeignKey(ref.Implantation,
6e7c919b
NC
1574 db_column='implantation',
1575 related_name='%(app_label)s_valeur_point')
9afaa55e 1576 # Méta
e9bbd6ba 1577 annee = models.IntegerField()
9afaa55e 1578
6e4600ef 1579 class Meta:
701f3bea 1580 ordering = ['-annee', 'implantation__nom']
6e7c919b 1581 abstract = True
c1195471
OL
1582 verbose_name = u"Valeur du point"
1583 verbose_name_plural = u"Valeurs du point"
8bb6f549 1584 unique_together = ('implantation', 'annee')
6e0bbb73 1585
9afaa55e 1586 def __unicode__(self):
fa1f7426
EMS
1587 return u'%s %s %s [%s] %s' % (
1588 self.devise.code, self.annee, self.valeur,
1589 self.implantation.nom_court, self.devise.nom
1590 )
6e7c919b
NC
1591
1592
1593class ValeurPoint(ValeurPoint_):
1594 __doc__ = ValeurPoint_.__doc__
1595
d104b0ae
EMS
1596reversion.register(ValeurPoint, format='xml')
1597
e9bbd6ba 1598
7013d234 1599class Devise(Archivable):
6e4600ef 1600 """
fa1f7426
EMS
1601 Devise monétaire.
1602 """
fa1f7426 1603 code = models.CharField(max_length=10, unique=True)
e9bbd6ba 1604 nom = models.CharField(max_length=255)
1605
6e4600ef 1606 class Meta:
1607 ordering = ['code']
7013d234
EMS
1608 verbose_name = u"devise"
1609 verbose_name_plural = u"devises"
ca1a7b76 1610
e9bbd6ba 1611 def __unicode__(self):
1612 return u'%s - %s' % (self.code, self.nom)
1613
d104b0ae
EMS
1614reversion.register(Devise, format='xml')
1615
fa1f7426 1616
45066657 1617class TypeContrat(models.Model):
fa1f7426
EMS
1618 """
1619 Type de contrat.
6e4600ef 1620 """
e9bbd6ba 1621 nom = models.CharField(max_length=255)
6e4600ef 1622 nom_long = models.CharField(max_length=255)
49f9f116 1623
8c1ae2b3 1624 class Meta:
1625 ordering = ['nom']
c1195471
OL
1626 verbose_name = u"Type de contrat"
1627 verbose_name_plural = u"Types de contrat"
8c1ae2b3 1628
9afaa55e 1629 def __unicode__(self):
1630 return u'%s' % (self.nom)
ca1a7b76 1631
d104b0ae
EMS
1632reversion.register(TypeContrat, format='xml')
1633
ca1a7b76 1634
2d4d2fcf 1635### AUTRES
1636
8c8ffc4f 1637class ResponsableImplantationProxy(ref.Implantation):
7bf28694 1638
6fb68b2f
DB
1639 def save(self):
1640 pass
1641
8c8ffc4f 1642 class Meta:
d7bf0cd3 1643 managed = False
8c8ffc4f
OL
1644 proxy = True
1645 verbose_name = u"Responsable d'implantation"
1646 verbose_name_plural = u"Responsables d'implantation"
1647
1648
1649class ResponsableImplantation(models.Model):
fa1f7426
EMS
1650 """
1651 Le responsable d'une implantation.
30be56d5 1652 Anciennement géré sur le Dossier du responsable.
1653 """
fa1f7426
EMS
1654 employe = models.ForeignKey(
1655 'Employe', db_column='employe', related_name='+', null=True,
1656 blank=True
1657 )
1658 implantation = models.OneToOneField(
1659 "ResponsableImplantationProxy", db_column='implantation',
1660 related_name='responsable', unique=True
1661 )
30be56d5 1662
1663 def __unicode__(self):
1664 return u'%s : %s' % (self.implantation, self.employe)
ca1a7b76 1665
30be56d5 1666 class Meta:
1667 ordering = ['implantation__nom']
8c1ae2b3 1668 verbose_name = "Responsable d'implantation"
1669 verbose_name_plural = "Responsables d'implantation"
d104b0ae
EMS
1670
1671reversion.register(ResponsableImplantation, format='xml')
edbc9e37
OL
1672
1673