rh.Employe méthodes
[auf_rh_dae.git] / project / rh / models.py
CommitLineData
e9bbd6ba 1# -=- encoding: utf-8 -=-
2
c267f20c 3from datetime import date
4
83b7692b 5from django.core.files.storage import FileSystemStorage
49f9f116 6from django.db import models
d6985a3a 7from django.conf import settings
c267f20c 8
d6985a3a
OL
9from auf.django.metadata.models import AUFMetadata
10from auf.django.metadata.managers import NoDeleteManager
83b7692b 11import datamaster_modeles.models as ref
b4aeadf3 12from validators import validate_date_passee
83b7692b 13
2d4d2fcf 14# Constantes
8c1ae2b3 15HELP_TEXT_DATE = "format: aaaa-mm-jj"
6e4600ef 16REGIME_TRAVAIL_DEFAULT = 100.00
17REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = 35.00
2d4d2fcf 18
19
83b7692b 20# Upload de fichiers
21storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
22 base_url=settings.PRIVE_MEDIA_URL)
23
24def poste_piece_dispatch(instance, filename):
25 path = "poste/%s/%s" % (instance.poste_id, filename)
26 return path
27
28def dossier_piece_dispatch(instance, filename):
29 path = "dossier/%s/%s" % (instance.dossier_id, filename)
30 return path
31
5ea6b5bb 32def employe_piece_dispatch(instance, filename):
33 path = "employe/%s/%s" % (instance.employe_id, filename)
34 return path
35
5f5a4f06 36
d6985a3a 37class Commentaire(AUFMetadata):
2d4d2fcf 38 texte = models.TextField()
6e4600ef 39 owner = models.ForeignKey('auth.User', db_column='owner', related_name='+')
2d4d2fcf 40
41 class Meta:
42 abstract = True
6e4600ef 43 ordering = ['-date_creation']
44
45 def __unicode__(self):
46 return u'%s' % (self.texte)
07b40eda 47
83b7692b 48
49### POSTE
50
51POSTE_APPEL_CHOICES = (
52 ('interne', 'Interne'),
53 ('externe', 'Externe'),
54)
55
d6985a3a 56class PosteManager(NoDeleteManager):
1f2979b8
OL
57 def get_query_set(self):
58 return super(PosteManager, self).get_query_set().select_related('implantation')
59
d6985a3a 60class Poste_(AUFMetadata):
6e4600ef 61 """Un Poste est un emploi (job) à combler dans une implantation.
62 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
63 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
64 """
1f2979b8
OL
65
66 objects = PosteManager()
67
83b7692b 68 # Identification
2d4d2fcf 69 nom = models.CharField(max_length=255,
8c1ae2b3 70 verbose_name="Titre du poste", )
2d4d2fcf 71 nom_feminin = models.CharField(max_length=255,
8c1ae2b3 72 verbose_name="Titre du poste (au féminin)",
6e4600ef 73 null=True)
74 implantation = models.ForeignKey(ref.Implantation,
75 db_column='implantation', related_name='+')
76 type_poste = models.ForeignKey('TypePoste', db_column='type_poste',
77 related_name='+',
78 null=True)
8277a35b 79 service = models.ForeignKey('Service', db_column='service', null=True,
6e4600ef 80 related_name='+',
8c1ae2b3 81 verbose_name="Direction/Service/Pôle support",
6e4600ef 82 default=1) # default = Rectorat
83 responsable = models.ForeignKey('Poste', db_column='responsable',
8277a35b 84 related_name='+', null=True,
8c1ae2b3 85 verbose_name="Poste du responsable",
6e4600ef 86 default=149) # default = Recteur
83b7692b 87
88 # Contrat
89 regime_travail = models.DecimalField(max_digits=12, decimal_places=2,
8277a35b 90 default=REGIME_TRAVAIL_DEFAULT, null=True,
8c1ae2b3 91 verbose_name="Temps de travail",
92 help_text="% du temps complet")
83b7692b 93 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
8277a35b 94 decimal_places=2, null=True,
2d4d2fcf 95 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
8c1ae2b3 96 verbose_name="Nb. heures par semaine")
83b7692b 97
98 # Recrutement
8277a35b
NC
99 local = models.NullBooleanField(verbose_name="Local", default=True,
100 null=True, blank=True)
101 expatrie = models.NullBooleanField(verbose_name="Expatrié", default=False,
102 null=True, blank=True)
103 mise_a_disposition = models.NullBooleanField(
8c1ae2b3 104 verbose_name="Mise à disposition",
8277a35b
NC
105 null=True, default=False)
106 appel = models.CharField(max_length=10, null=True,
8c1ae2b3 107 verbose_name="Appel à candidature",
6e4600ef 108 choices=POSTE_APPEL_CHOICES,
109 default='interne')
83b7692b 110
111 # Rémunération
6e4600ef 112 classement_min = models.ForeignKey('Classement',
113 db_column='classement_min', related_name='+',
114 null=True, blank=True)
115 classement_max = models.ForeignKey('Classement',
116 db_column='classement_max', related_name='+',
117 null=True, blank=True)
118 valeur_point_min = models.ForeignKey('ValeurPoint',
119 db_column='valeur_point_min', related_name='+',
120 null=True, blank=True)
121 valeur_point_max = models.ForeignKey('ValeurPoint',
122 db_column='valeur_point_max', related_name='+',
123 null=True, blank=True)
8277a35b 124 devise_min = models.ForeignKey('Devise', db_column='devise_min', null=True,
6e4600ef 125 related_name='+', default=5)
8277a35b 126 devise_max = models.ForeignKey('Devise', db_column='devise_max', null=True,
6e4600ef 127 related_name='+', default=5)
83b7692b 128 salaire_min = models.DecimalField(max_digits=12, decimal_places=2,
8277a35b 129 null=True, default=0)
83b7692b 130 salaire_max = models.DecimalField(max_digits=12, decimal_places=2,
8277a35b 131 null=True, default=0)
83b7692b 132 indemn_min = models.DecimalField(max_digits=12, decimal_places=2,
8277a35b 133 null=True, default=0)
83b7692b 134 indemn_max = models.DecimalField(max_digits=12, decimal_places=2,
8277a35b 135 null=True, default=0)
83b7692b 136 autre_min = models.DecimalField(max_digits=12, decimal_places=2,
8277a35b 137 null=True, default=0)
83b7692b 138 autre_max = models.DecimalField(max_digits=12, decimal_places=2,
8277a35b 139 null=True, default=0)
83b7692b 140
141 # Comparatifs de rémunération
8277a35b 142 devise_comparaison = models.ForeignKey('Devise', null=True,
6e4600ef 143 db_column='devise_comparaison',
144 related_name='+',
145 default=5)
83b7692b 146 comp_locale_min = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 147 null=True, blank=True)
83b7692b 148 comp_locale_max = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 149 null=True, blank=True)
83b7692b 150 comp_universite_min = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 151 null=True, blank=True)
83b7692b 152 comp_universite_max = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 153 null=True, blank=True)
83b7692b 154 comp_fonctionpub_min = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 155 null=True, blank=True)
83b7692b 156 comp_fonctionpub_max = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 157 null=True, blank=True)
83b7692b 158 comp_ong_min = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 159 null=True, blank=True)
83b7692b 160 comp_ong_max = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 161 null=True, blank=True)
83b7692b 162 comp_autre_min = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 163 null=True, blank=True)
83b7692b 164 comp_autre_max = models.DecimalField(max_digits=12, decimal_places=2,
6e4600ef 165 null=True, blank=True)
83b7692b 166
167 # Justification
6e4600ef 168 justification = models.TextField(null=True, blank=True)
83b7692b 169
2d4d2fcf 170 # Autres Metadata
07b40eda 171 date_validation = models.DateTimeField(null=True, blank=True) # de dae
8c1ae2b3 172 date_debut = models.DateField(verbose_name="Date de début",
7332d8f9
OL
173 help_text=HELP_TEXT_DATE,
174 null=True, blank=True)
8c1ae2b3 175 date_fin = models.DateField(verbose_name="Date de fin",
6e4600ef 176 help_text=HELP_TEXT_DATE,
177 null=True, blank=True)
178
179 class Meta:
37868f0b 180 abstract = True
6e4600ef 181 ordering = ['implantation__nom', 'nom']
8c1ae2b3 182 verbose_name = "Poste"
183 verbose_name_plural = "Postes"
6e4600ef 184
83b7692b 185 def __unicode__(self):
8c1ae2b3 186 representation = u'%s - %s [%s]' % (self.implantation, self.nom,
187 self.id)
188 if self.is_vacant():
189 representation = representation + u' (vacant)'
190 return representation
191
192 def is_vacant(self):
193 # TODO : si existe un dossier actif pour ce poste, return False
194 # self.dossier_set.all() fonctionne pas
195 return False
83b7692b 196
197
37868f0b
NC
198class Poste(Poste_):
199 __doc__ = Poste_.__doc__
200
201
769a0755
NC
202class Poste(Poste_):
203 __doc__ = Poste_.__doc__
204
205
83b7692b 206POSTE_FINANCEMENT_CHOICES = (
207 ('A', 'A - Frais de personnel'),
208 ('B', 'B - Projet(s)-Titre(s)'),
209 ('C', 'C - Autre')
210)
211
6e7c919b
NC
212
213class PosteFinancement_(models.Model):
6e4600ef 214 """Pour un Poste, structure d'informations décrivant comment on prévoit
215 financer ce Poste.
216 """
217 poste = models.ForeignKey('Poste', db_column='poste',
6e7c919b 218 related_name='%(app_label)s_financements')
83b7692b 219 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
220 pourcentage = models.DecimalField(max_digits=12, decimal_places=2,
8c1ae2b3 221 help_text="ex.: 33.33 % (décimale avec point)")
83b7692b 222 commentaire = models.TextField(
8c1ae2b3 223 help_text="Spécifiez la source de financement.")
83b7692b 224
225 class Meta:
6e7c919b 226 abstract = True
83b7692b 227 ordering = ['type']
6e4600ef 228
229 def __unicode__(self):
230 return u'%s : %s %' % (self.type, self.pourcentage)
83b7692b 231
6e7c919b
NC
232
233class PosteFinancement(PosteFinancement_):
234 __doc__ = PosteFinancement_.__doc__
235
236
83b7692b 237class PostePiece(models.Model):
6e4600ef 238 """Documents relatifs au Poste.
7abc6d45 239 Ex.: Description de poste
240 """
8c1ae2b3 241 poste = models.ForeignKey('Poste', db_column='poste',
6e4600ef 242 related_name='pieces')
8c1ae2b3 243 nom = models.CharField(verbose_name="Nom", max_length=255)
244 fichier = models.FileField(verbose_name="Fichier",
83b7692b 245 upload_to=poste_piece_dispatch,
246 storage=storage_prive)
247
6e4600ef 248 class Meta:
249 ordering = ['nom']
250
251 def __unicode__(self):
252 return u'%s' % (self.nom)
253
068d1462
OL
254class PosteComparaison(models.Model):
255 """
256 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
257 """
258 poste = models.ForeignKey('Poste', related_name='comparaisons_internes')
0f0dacbb 259 implantation = models.ForeignKey(ref.Implantation, null=True, blank=True, related_name="+")
068d1462
OL
260 nom = models.CharField(verbose_name="Poste", max_length=255, null=True, blank=True)
261 montant = models.IntegerField(null=True)
262 devise = models.ForeignKey("Devise", default=5, related_name='+', null=True, blank=True)
1d0f4eef
OL
263
264 def taux_devise(self):
265 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.implantation)
266 if len(liste_taux) == 0:
267 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.implantation))
268 else:
269 return liste_taux[0].taux
270
271 def montant_euros(self):
272 return round(float(self.montant) * float(self.taux_devise()), 2)
068d1462
OL
273
274
07b40eda 275class PosteCommentaire(Commentaire):
8c1ae2b3 276 poste = models.ForeignKey('Poste', db_column='poste', related_name='+')
83b7692b 277
2d4d2fcf 278
83b7692b 279### EMPLOYÉ/PERSONNE
e9bbd6ba 280
281GENRE_CHOICES = (
282 ('M', 'Homme'),
283 ('F', 'Femme'),
284)
285SITUATION_CHOICES = (
286 ('C', 'Célibataire'),
287 ('F', 'Fiancé'),
288 ('M', 'Marié'),
289)
290
d6985a3a 291class Employe(AUFMetadata):
6e4600ef 292 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
293 Dossiers qu'il occupe ou a occupé de Postes.
294
295 Cette classe aurait pu avantageusement s'appeler Personne car la notion
296 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
297 """
9afaa55e 298 # Identification
e9bbd6ba 299 nom = models.CharField(max_length=255)
8c1ae2b3 300 prenom = models.CharField(max_length=255, verbose_name="Prénom")
6e4600ef 301 nom_affichage = models.CharField(max_length=255,
8c1ae2b3 302 verbose_name="Nom d'affichage",
6e4600ef 303 null=True, blank=True)
83b7692b 304 nationalite = models.ForeignKey(ref.Pays, to_field='code',
6e4600ef 305 db_column='nationalite',
8c1ae2b3 306 related_name='employes_nationalite',
307 verbose_name="Nationalité")
6e4600ef 308 date_naissance = models.DateField(help_text=HELP_TEXT_DATE,
8c1ae2b3 309 verbose_name="Date de naissance",
b4aeadf3 310 validators=[validate_date_passee],
6e4600ef 311 null=True, blank=True)
2d4d2fcf 312 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
83b7692b 313
9afaa55e 314 # Infos personnelles
6e4600ef 315 situation_famille = models.CharField(max_length=1,
316 choices=SITUATION_CHOICES,
8c1ae2b3 317 verbose_name="Situation familiale",
6e4600ef 318 null=True, blank=True)
8c1ae2b3 319 date_entree = models.DateField(verbose_name="Date d'entrée à l'AUF",
6e4600ef 320 help_text=HELP_TEXT_DATE,
7abc6d45 321 null=True, blank=True)
83b7692b 322
9afaa55e 323 # Coordonnées
8c1ae2b3 324 tel_domicile = models.CharField(max_length=255,
325 verbose_name="Tél. domicile",
326 null=True, blank=True)
327 tel_cellulaire = models.CharField(max_length=255,
328 verbose_name="Tél. cellulaire",
329 null=True, blank=True)
e9bbd6ba 330 adresse = models.CharField(max_length=255, null=True, blank=True)
e9bbd6ba 331 ville = models.CharField(max_length=255, null=True, blank=True)
332 province = models.CharField(max_length=255, null=True, blank=True)
333 code_postal = models.CharField(max_length=255, null=True, blank=True)
6e4600ef 334 pays = models.ForeignKey(ref.Pays, to_field='code', db_column='pays',
335 related_name='employes',
336 null=True, blank=True)
9afaa55e 337
6e4600ef 338 class Meta:
67666927 339 ordering = ['nom_affichage','nom','prenom']
8c1ae2b3 340 verbose_name = "Employé"
341 verbose_name_plural = "Employés"
6e4600ef 342
9afaa55e 343 def __unicode__(self):
8c1ae2b3 344 return u'%s' % (self.get_nom())
345
346 def get_nom(self):
ebc8eb32 347 nom_affichage = self.nom_affichage
348 if not nom_affichage:
349 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
8c1ae2b3 350 return nom_affichage
d04d084c 351
352 def civilite(self):
353 civilite = u''
354 if self.genre.upper() == u'M':
355 civilite = u'M.'
356 elif self.genre.upper() == u'F':
357 civilite = u'Mme'
358 return civilite
5ea6b5bb 359
360 def url_photo(self):
361 """Retourne l'URL du service retournant la photo de l'Employe.
362 Équivalent reverse url 'rh_photo' avec id en param.
363 """
364 from django.core.urlresolvers import reverse
365 return reverse('rh_photo', kwargs={'id':self.id})
35c0c2fe 366
c267f20c 367 def dossiers_passes(self):
368 today = date.today()
369 return self.dossiers.filter(date_fin__lt=today).order_by('-date_fin')
370
371 def dossiers_futurs(self):
372 today = date.today()
373 return self.dossiers.filter(date_debut__gt=today).order_by('-date_fin')
374
375 def dossiers_encours(self):
376 dossiers_p_f = self.dossiers_passes() | self.dossiers_futurs()
377 ids_dossiers_p_f = [d.id for d in dossiers_p_f]
378 return self.dossiers.exclude(id__in=ids_dossiers_p_f).order_by('-date_fin')
35c0c2fe 379
380 def postes_encours(self):
381 postes_encours = set()
382 for d in self.dossiers_encours():
383 postes_encours.add(d.poste)
384 return postes_encours
385
386 def poste_principal(self):
387 return self.dossiers_encours()[0].poste
9afaa55e 388
7abc6d45 389class EmployePiece(models.Model):
6e4600ef 390 """Documents relatifs à un employé.
7abc6d45 391 Ex.: CV...
392 """
8c1ae2b3 393 employe = models.ForeignKey('Employe', db_column='employe',
6e4600ef 394 related_name='+')
8c1ae2b3 395 nom = models.CharField(verbose_name="Nom", max_length=255)
396 fichier = models.FileField(verbose_name="Fichier",
5ea6b5bb 397 upload_to=employe_piece_dispatch,
7abc6d45 398 storage=storage_prive)
399
6e4600ef 400 class Meta:
401 ordering = ['nom']
402
403 def __unicode__(self):
404 return u'%s' % (self.nom)
405
07b40eda 406class EmployeCommentaire(Commentaire):
8c1ae2b3 407 employe = models.ForeignKey('Employe', db_column='employe',
6e4600ef 408 related_name='+')
9afaa55e 409
2d4d2fcf 410
e9bbd6ba 411LIEN_PARENTE_CHOICES = (
412 ('Conjoint', 'Conjoint'),
413 ('Conjointe', 'Conjointe'),
414 ('Fille', 'Fille'),
415 ('Fils', 'Fils'),
416)
417
d6985a3a 418class AyantDroit(AUFMetadata):
6e4600ef 419 """Personne en relation avec un Employe.
420 """
9afaa55e 421 # Identification
e9bbd6ba 422 nom = models.CharField(max_length=255)
8c1ae2b3 423 prenom = models.CharField(max_length=255,
424 verbose_name="Prénom",)
6e4600ef 425 nom_affichage = models.CharField(max_length=255,
8c1ae2b3 426 verbose_name="Nom d'affichage",
6e4600ef 427 null=True, blank=True)
2d4d2fcf 428 nationalite = models.ForeignKey(ref.Pays, to_field='code',
6e4600ef 429 db_column='nationalite',
8c1ae2b3 430 related_name='ayantdroits_nationalite',
431 verbose_name="Nationalité")
6e4600ef 432 date_naissance = models.DateField(help_text=HELP_TEXT_DATE,
8c1ae2b3 433 verbose_name="Date de naissance",
25037368 434 validators=[validate_date_passee],
6e4600ef 435 null=True, blank=True)
2d4d2fcf 436 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
83b7692b 437
9afaa55e 438 # Relation
439 employe = models.ForeignKey('Employe', db_column='employe',
8c1ae2b3 440 related_name='ayantdroits',
441 verbose_name="Employé")
6e4600ef 442 lien_parente = models.CharField(max_length=10,
443 choices=LIEN_PARENTE_CHOICES,
8c1ae2b3 444 verbose_name="Lien de parenté",
6e4600ef 445 null=True, blank=True)
446
447 class Meta:
448 ordering = ['nom_affichage']
8c1ae2b3 449 verbose_name = "Ayant droit"
450 verbose_name_plural = "Ayants droit"
451
6e4600ef 452 def __unicode__(self):
8c1ae2b3 453 return u'%s' % (self.get_nom())
454
455 def get_nom(self):
456 nom_affichage = self.nom_affichage
457 if not nom_affichage:
458 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
459 return nom_affichage
83b7692b 460
07b40eda 461class AyantDroitCommentaire(Commentaire):
8c1ae2b3 462 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
6e4600ef 463 related_name='+')
83b7692b 464
2d4d2fcf 465
83b7692b 466### DOSSIER
467
468STATUT_RESIDENCE_CHOICES = (
469 ('local', 'Local'),
470 ('expat', 'Expatrié'),
471)
472
473COMPTE_COMPTA_CHOICES = (
474 ('coda', 'CODA'),
475 ('scs', 'SCS'),
476 ('aucun', 'Aucun'),
477)
478
d6985a3a 479class Dossier_(AUFMetadata):
6e4600ef 480 """Le Dossier regroupe les informations relatives à l'occupation
481 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
482 par un Employe.
483
484 Plusieurs Contrats peuvent être associés au Dossier.
485 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
486 lequel aucun Dossier n'existe est un poste vacant.
487 """
83b7692b 488 # Identification
6e4600ef 489 employe = models.ForeignKey('Employe', db_column='employe',
d04d084c 490 related_name='dossiers',
8c1ae2b3 491 verbose_name="Employé")
1f2979b8 492 poste = models.ForeignKey('Poste', db_column='poste', related_name='+')
8277a35b
NC
493 statut = models.ForeignKey('Statut', related_name='+', default=3,
494 null=True)
83b7692b 495 organisme_bstg = models.ForeignKey('OrganismeBstg',
6e4600ef 496 db_column='organisme_bstg',
497 related_name='+',
8c1ae2b3 498 verbose_name="Organisme",
499 help_text="Si détaché (DET) ou \
6e4600ef 500 mis à disposition (MAD), \
501 préciser l'organisme.",
502 null=True, blank=True)
503
83b7692b 504 # Recrutement
2d4d2fcf 505 remplacement = models.BooleanField(default=False)
83b7692b 506 statut_residence = models.CharField(max_length=10, default='local',
8277a35b 507 verbose_name="Statut", null=True,
2d4d2fcf 508 choices=STATUT_RESIDENCE_CHOICES)
83b7692b 509
510 # Rémunération
6e4600ef 511 classement = models.ForeignKey('Classement', db_column='classement',
512 related_name='+',
513 null=True, blank=True)
8277a35b 514 regime_travail = models.DecimalField(max_digits=12, null=True,
2d4d2fcf 515 decimal_places=2,
516 default=REGIME_TRAVAIL_DEFAULT,
8c1ae2b3 517 verbose_name="Régime de travail",
518 help_text="% du temps complet")
83b7692b 519 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
8277a35b 520 decimal_places=2, null=True,
2d4d2fcf 521 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
8c1ae2b3 522 verbose_name="Nb. heures par semaine")
7abc6d45 523
524 # Occupation du Poste par cet Employe (anciennement "mandat")
8c1ae2b3 525 date_debut = models.DateField(verbose_name="Date de début d'occupation \
6e4600ef 526 de poste",
527 help_text=HELP_TEXT_DATE)
8c1ae2b3 528 date_fin = models.DateField(verbose_name="Date de fin d'occupation \
6e4600ef 529 de poste",
2d4d2fcf 530 help_text=HELP_TEXT_DATE,
6e4600ef 531 null=True, blank=True)
e9bbd6ba 532
2d4d2fcf 533 # Comptes
534 # TODO?
83b7692b 535
6e4600ef 536 class Meta:
37868f0b 537 abstract = True
49449367 538 ordering = ['employe__nom', ]
8c1ae2b3 539 verbose_name = "Dossier"
540 verbose_name_plural = "Dossiers"
6e4600ef 541
83b7692b 542 def __unicode__(self):
8c1ae2b3 543 poste = self.poste.nom
544 if self.employe.genre == 'F':
545 poste = self.poste.nom_feminin
546 return u'%s - %s' % (self.employe, poste)
83b7692b 547
37868f0b
NC
548
549class Dossier(Dossier_):
550 __doc__ = Dossier_.__doc__
551
552
83b7692b 553class DossierPiece(models.Model):
7abc6d45 554 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
555 Ex.: Lettre de motivation.
556 """
8c1ae2b3 557 dossier = models.ForeignKey('Dossier', db_column='dossier',
6e4600ef 558 related_name='+')
8c1ae2b3 559 nom = models.CharField(verbose_name="Nom", max_length=255)
560 fichier = models.FileField(verbose_name="Fichier",
83b7692b 561 upload_to=dossier_piece_dispatch,
562 storage=storage_prive)
563
6e4600ef 564 class Meta:
565 ordering = ['nom']
566
567 def __unicode__(self):
568 return u'%s' % (self.nom)
569
07b40eda 570class DossierCommentaire(Commentaire):
8c1ae2b3 571 dossier = models.ForeignKey('Dossier', db_column='dossier',
6e4600ef 572 related_name='+')
83b7692b 573
1d0f4eef
OL
574class DossierComparaison(models.Model):
575 """
576 Photo d'une comparaison salariale au moment de l'embauche.
577 """
578 dossier = models.ForeignKey('Dossier', related_name='comparaisons')
8c6269cc 579 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
1d0f4eef
OL
580 poste = models.CharField(max_length=255, null=True, blank=True)
581 personne = models.CharField(max_length=255, null=True, blank=True)
582 montant = models.IntegerField(null=True)
93494cf1 583 devise = models.ForeignKey('Devise', default=5, related_name='+', null=True, blank=True)
1d0f4eef
OL
584
585 def taux_devise(self):
586 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.dossier.poste.implantation)
587 if len(liste_taux) == 0:
588 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.dossier.poste.implantation))
589 else:
590 return liste_taux[0].taux
591
592 def montant_euros(self):
593 return round(float(self.montant) * float(self.taux_devise()), 2)
594
2d4d2fcf 595
07b40eda 596### RÉMUNÉRATION
e9bbd6ba 597
d6985a3a 598class RemunerationMixin(AUFMetadata):
9afaa55e 599 # Identification
6e7c919b
NC
600 dossier = models.ForeignKey('Dossier', db_column='dossier',
601 related_name='%(app_label)s_%(class)s_remunerations')
83b7692b 602 type = models.ForeignKey('TypeRemuneration', db_column='type',
8c1ae2b3 603 related_name='+',
604 verbose_name="Type de rémunération")
7abc6d45 605 type_revalorisation = models.ForeignKey('TypeRevalorisation',
606 db_column='type_revalorisation',
6e4600ef 607 related_name='+',
8c1ae2b3 608 verbose_name="Type de revalorisation",
7abc6d45 609 null=True, blank=True)
50fa9bc1 610 montant = models.FloatField(null=True, blank=True,
6e4600ef 611 default=0)
2d4d2fcf 612 # Annuel (12 mois, 52 semaines, 364 jours?)
c589d980 613 devise = models.ForeignKey('Devise', to_field='id',
6e4600ef 614 db_column='devise', related_name='+',
615 default=5)
2d4d2fcf 616 # commentaire = precision
617 commentaire = models.CharField(max_length=255, null=True, blank=True)
618 # date_debut = anciennement date_effectif
8c1ae2b3 619 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
620 verbose_name="Date de début",
6e4600ef 621 null=True, blank=True)
622 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
8c1ae2b3 623 verbose_name="Date de fin",
6e4600ef 624 null=True, blank=True)
83b7692b 625
2d4d2fcf 626 class Meta:
627 abstract = True
6e4600ef 628 ordering = ['type__nom', '-date_fin']
629
630 def __unicode__(self):
631 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
2d4d2fcf 632
6e7c919b 633class Remuneration_(RemunerationMixin):
2d4d2fcf 634 """Structure de rémunération (données budgétaires) en situation normale
635 pour un Dossier. Si un Evenement existe, utiliser la structure de
636 rémunération EvenementRemuneration de cet événement.
637 """
83b7692b 638
639 def montant_mois(self):
640 return round(self.montant / 12, 2)
641
642 def taux_devise(self):
643 return self.devise.tauxchange_set.order_by('-annee').all()[0].taux
644
645 def montant_euro(self):
646 return round(float(self.montant) / float(self.taux_devise()), 2)
647
648 def montant_euro_mois(self):
649 return round(self.montant_euro() / 12, 2)
9afaa55e 650
651 def __unicode__(self):
652 try:
653 devise = self.devise.code
654 except:
655 devise = "???"
656 return "%s %s" % (self.montant, devise)
83b7692b 657
6e7c919b
NC
658 class Meta:
659 abstract = True
8c1ae2b3 660 verbose_name = "Rémunération"
661 verbose_name_plural = "Rémunérations"
6e7c919b
NC
662
663
664class Remuneration(Remuneration_):
665 __doc__ = Remuneration_.__doc__
666
2d4d2fcf 667
668### CONTRATS
c41b7fcc
OL
669
670class ContratManager(NoDeleteManager):
671 def get_query_set(self):
672 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
673
2d4d2fcf 674
d6985a3a 675class Contrat(AUFMetadata):
2d4d2fcf 676 """Document juridique qui encadre la relation de travail d'un Employe
677 pour un Poste particulier. Pour un Dossier (qui documente cette
678 relation de travail) plusieurs contrats peuvent être associés.
679 """
c41b7fcc
OL
680
681 objects = ContratManager()
682
6e4600ef 683 dossier = models.ForeignKey('Dossier', db_column='dossier',
684 related_name='+')
685 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
8c1ae2b3 686 related_name='+',
687 verbose_name="Type de contrat")
688 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
689 verbose_name="Date de début")
6e4600ef 690 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
8c1ae2b3 691 verbose_name="Date de fin",
6e4600ef 692 null=True, blank=True)
693
694 class Meta:
695 ordering = ['dossier__employe__nom_affichage']
8c1ae2b3 696 verbose_name = "Contrat"
697 verbose_name_plural = "Contrats"
6e4600ef 698
699 def __unicode__(self):
8c1ae2b3 700 return u'%s - %s' % (self.dossier, self.id)
6e4600ef 701
702# TODO? class ContratPiece(models.Model):
2d4d2fcf 703
704
705### ÉVÉNEMENTS
706
d6985a3a 707class Evenement_(AUFMetadata):
6e4600ef 708 """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
709 d'un Dossier qui vient altérer des informations normales liées à un Dossier
710 (ex.: la Remuneration).
711
712 Ex.: congé de maternité, maladie...
713
714 Lors de ces situations exceptionnelles, l'Employe a un régime de travail
715 différent et une rémunération en conséquence. On souhaite toutefois
716 conserver le Dossier intact afin d'éviter une re-saisie des données lors
717 du retour à la normale.
718 """
8c1ae2b3 719 dossier = models.ForeignKey('Dossier', db_column='dossier',
6e4600ef 720 related_name='+')
721 nom = models.CharField(max_length=255)
8c1ae2b3 722 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
723 verbose_name="Date de début")
724 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
725 verbose_name="Date de fin",
6e4600ef 726 null=True, blank=True)
6e7c919b 727
6e4600ef 728 class Meta:
6e7c919b 729 abstract = True
6e4600ef 730 ordering = ['nom']
8c1ae2b3 731 verbose_name = "Évènement"
732 verbose_name_plural = "Évènements"
6e4600ef 733
734 def __unicode__(self):
735 return u'%s' % (self.nom)
6e7c919b
NC
736
737
738class Evenement(Evenement_):
739 __doc__ = Evenement_.__doc__
740
2d4d2fcf 741
6e7c919b 742class EvenementRemuneration_(RemunerationMixin):
6e4600ef 743 """Structure de rémunération liée à un Evenement qui remplace
744 temporairement la Remuneration normale d'un Dossier, pour toute la durée
745 de l'Evenement.
746 """
747 evenement = models.ForeignKey("Evenement", db_column='evenement',
8c1ae2b3 748 related_name='+',
749 verbose_name="Évènement")
750 # TODO : le champ dossier hérité de Remuneration doit être dérivé
751 # de l'Evenement associé
83b7692b 752
6e7c919b
NC
753 class Meta:
754 abstract = True
8c1ae2b3 755 ordering = ['evenement', 'type__nom', '-date_fin']
756 verbose_name = "Évènement - rémunération"
757 verbose_name_plural = "Évènements - rémunérations"
6e7c919b
NC
758
759
760class EvenementRemuneration(EvenementRemuneration_):
761 __doc__ = EvenementRemuneration_.__doc__
83b7692b 762
f31ddfa0
NC
763 class Meta:
764 abstract = True
765
766
767class EvenementRemuneration(EvenementRemuneration_):
768 __doc__ = EvenementRemuneration_.__doc__
769
83b7692b 770
771### RÉFÉRENCES RH
772
d6985a3a 773class FamilleEmploi(AUFMetadata):
6e4600ef 774 """Catégorie utilisée dans la gestion des Postes.
775 Catégorie supérieure à TypePoste.
776 """
e9bbd6ba 777 nom = models.CharField(max_length=255)
6e4600ef 778
8c1ae2b3 779 class Meta:
780 ordering = ['nom']
781 verbose_name = "Famille d'emploi"
782 verbose_name_plural = "Familles d'emploi"
783
6e4600ef 784 def __unicode__(self):
785 return u'%s' % (self.nom)
e9bbd6ba 786
d6985a3a 787class TypePoste(AUFMetadata):
6e4600ef 788 """Catégorie de Poste.
789 """
e9bbd6ba 790 nom = models.CharField(max_length=255)
8c1ae2b3 791 nom_feminin = models.CharField(max_length=255,
792 verbose_name="Nom féminin")
6e4600ef 793
8c1ae2b3 794 is_responsable = models.BooleanField(default=False,
795 verbose_name="Poste de responsabilité")
9afaa55e 796 famille_emploi = models.ForeignKey('FamilleEmploi',
6e4600ef 797 db_column='famille_emploi',
8c1ae2b3 798 related_name='+',
799 verbose_name="Famille d'emploi")
e9bbd6ba 800
6e4600ef 801 class Meta:
802 ordering = ['nom']
8c1ae2b3 803 verbose_name = "Type de poste"
804 verbose_name_plural = "Types de poste"
6e4600ef 805
e9bbd6ba 806 def __unicode__(self):
6e4600ef 807 return u'%s' % (self.nom)
e9bbd6ba 808
809
810TYPE_PAIEMENT_CHOICES = (
811 ('Régulier', 'Régulier'),
812 ('Ponctuel', 'Ponctuel'),
813)
814
815NATURE_REMUNERATION_CHOICES = (
816 ('Accessoire', 'Accessoire'),
817 ('Charges', 'Charges'),
818 ('Indemnité', 'Indemnité'),
7abc6d45 819 ('RAS', 'Rémunération autre source'),
e9bbd6ba 820 ('Traitement', 'Traitement'),
821)
822
d6985a3a 823class TypeRemuneration(AUFMetadata):
6e4600ef 824 """Catégorie de Remuneration.
825 """
e9bbd6ba 826 nom = models.CharField(max_length=255)
9afaa55e 827 type_paiement = models.CharField(max_length=30,
8c1ae2b3 828 choices=TYPE_PAIEMENT_CHOICES,
829 verbose_name="Type de paiement")
9afaa55e 830 nature_remuneration = models.CharField(max_length=30,
8c1ae2b3 831 choices=NATURE_REMUNERATION_CHOICES,
832 verbose_name="Nature de la rémunération")
833
834 class Meta:
835 ordering = ['nom']
836 verbose_name = "Type de rémunération"
837 verbose_name_plural = "Types de rémunération"
9afaa55e 838
839 def __unicode__(self):
6e4600ef 840 return u'%s' % (self.nom)
e9bbd6ba 841
d6985a3a 842class TypeRevalorisation(AUFMetadata):
7abc6d45 843 """Justification du changement de la Remuneration.
6e4600ef 844 (Actuellement utilisé dans aucun traitement informatique.)
7abc6d45 845 """
e9bbd6ba 846 nom = models.CharField(max_length=255)
8c1ae2b3 847
848 class Meta:
849 ordering = ['nom']
850 verbose_name = "Type de revalorisation"
851 verbose_name_plural = "Types de revalorisation"
e9bbd6ba 852
853 def __unicode__(self):
6e4600ef 854 return u'%s' % (self.nom)
855
d6985a3a 856class Service(AUFMetadata):
6e4600ef 857 """Unité administrative où les Postes sont rattachés.
858 """
859 nom = models.CharField(max_length=255)
9afaa55e 860
861 class Meta:
862 ordering = ['nom']
8c1ae2b3 863 verbose_name = "Service"
864 verbose_name_plural = "Services"
e9bbd6ba 865
6e4600ef 866 def __unicode__(self):
867 return u'%s' % (self.nom)
868
e9bbd6ba 869
870TYPE_ORGANISME_CHOICES = (
871 ('MAD', 'Mise à disposition'),
872 ('DET', 'Détachement'),
873)
874
d6985a3a 875class OrganismeBstg(AUFMetadata):
6e4600ef 876 """Organisation d'où provient un Employe mis à disposition (MAD) de
877 ou détaché (DET) à l'AUF à titre gratuit.
878
879 (BSTG = bien et service à titre gratuit.)
880 """
e9bbd6ba 881 nom = models.CharField(max_length=255)
882 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
6e4600ef 883 pays = models.ForeignKey(ref.Pays, to_field='code',
884 db_column='pays',
885 related_name='organismes_bstg',
886 null=True, blank=True)
9afaa55e 887
888 class Meta:
889 ordering = ['type', 'nom']
8c1ae2b3 890 verbose_name = "Organisme BSTG"
891 verbose_name_plural = "Organismes BSTG"
9afaa55e 892
6e4600ef 893 def __unicode__(self):
8c1ae2b3 894 return u'%s (%s)' % (self.nom, self.get_type_display())
83b7692b 895
d6985a3a 896class Statut(AUFMetadata):
6e4600ef 897 """Statut de l'Employe dans le cadre d'un Dossier particulier.
898 """
9afaa55e 899 # Identification
e9bbd6ba 900 code = models.CharField(max_length=25, unique=True)
901 nom = models.CharField(max_length=255)
e9bbd6ba 902
6e4600ef 903 class Meta:
904 ordering = ['code']
8c1ae2b3 905 verbose_name = "Statut d'employé"
906 verbose_name_plural = "Statuts d'employé"
6e4600ef 907
9afaa55e 908 def __unicode__(self):
909 return u'%s : %s' % (self.code, self.nom)
910
83b7692b 911
e9bbd6ba 912TYPE_CLASSEMENT_CHOICES = (
6e4600ef 913 ('S', 'S -Soutien'),
914 ('T', 'T - Technicien'),
915 ('P', 'P - Professionel'),
916 ('C', 'C - Cadre'),
917 ('D', 'D - Direction'),
918 ('SO', 'SO - Sans objet [expatriés]'),
919 ('HG', 'HG - Hors grille [direction]'),
e9bbd6ba 920)
83b7692b 921
6e7c919b 922
d6985a3a 923class Classement_(AUFMetadata):
6e4600ef 924 """Éléments de classement de la
925 "Grille générique de classement hiérarchique".
926
927 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
928 classement dans la grille. Le classement donne le coefficient utilisé dans:
929
930 salaire de base = coefficient * valeur du point de l'Implantation du Poste
931 """
9afaa55e 932 # Identification
e9bbd6ba 933 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
8c1ae2b3 934 echelon = models.IntegerField(verbose_name="Échelon")
935 degre = models.IntegerField(verbose_name="Degré")
8277a35b
NC
936 coefficient = models.FloatField(default=0, verbose_name="Coéfficient",
937 null=True)
9afaa55e 938 # Méta
6e4600ef 939 # annee # au lieu de date_debut et date_fin
940 commentaire = models.TextField(null=True, blank=True)
941
942 class Meta:
6e7c919b 943 abstract = True
6e4600ef 944 ordering = ['type','echelon','degre','coefficient']
8c1ae2b3 945 verbose_name = "Classement"
946 verbose_name_plural = "Classements"
e9bbd6ba 947
948 def __unicode__(self):
949 return u'%s.%s.%s (%s)' % (self.type, self.echelon, self.degre,
950 self.coefficient)
951
6e7c919b
NC
952class Classement(Classement_):
953 __doc__ = Classement_.__doc__
954
955
d6985a3a 956class TauxChange_(AUFMetadata):
7abc6d45 957 """Taux de change de la devise vers l'euro (EUR)
6e4600ef 958 pour chaque année budgétaire.
7abc6d45 959 """
9afaa55e 960 # Identification
8c1ae2b3 961 devise = models.ForeignKey('Devise', db_column='devise',
6e4600ef 962 related_name='+')
8c1ae2b3 963 annee = models.IntegerField(verbose_name="Année")
964 taux = models.FloatField(verbose_name="Taux vers l'euro")
6e7c919b 965
6e4600ef 966 class Meta:
6e7c919b 967 abstract = True
8c1ae2b3 968 ordering = ['-annee', 'devise__code']
969 verbose_name = "Taux de change"
970 verbose_name_plural = "Taux de change"
6e4600ef 971
972 def __unicode__(self):
8c1ae2b3 973 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
e9bbd6ba 974
6e7c919b
NC
975
976class TauxChange(TauxChange_):
977 __doc__ = TauxChange_.__doc__
978
701f3bea
OL
979class ValeurPointManager(NoDeleteManager):
980 def get_query_set(self):
981 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
982
6e7c919b 983
d6985a3a 984class ValeurPoint_(AUFMetadata):
6e4600ef 985 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
986 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
8c1ae2b3 987 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
6e4600ef 988
989 salaire de base = coefficient * valeur du point de l'Implantation du Poste
990 """
701f3bea
OL
991
992 objects = ValeurPointManager()
993
8277a35b
NC
994 valeur = models.FloatField(null=True)
995 devise = models.ForeignKey('Devise', db_column='devise', null=True,
6e4600ef 996 related_name='+', default=5)
83b7692b 997 implantation = models.ForeignKey(ref.Implantation,
6e7c919b
NC
998 db_column='implantation',
999 related_name='%(app_label)s_valeur_point')
9afaa55e 1000 # Méta
e9bbd6ba 1001 annee = models.IntegerField()
9afaa55e 1002
6e4600ef 1003 class Meta:
701f3bea 1004 ordering = ['-annee', 'implantation__nom']
6e7c919b 1005 abstract = True
8c1ae2b3 1006 verbose_name = "Valeur du point"
1007 verbose_name_plural = "Valeurs du point"
6e0bbb73 1008
e9d7483c 1009 # TODO : cette fonction n'était pas présente dans la branche dev, utilité?
ee23ecbc
NC
1010 def get_tauxchange_courant(self):
1011 """
1012 Recherche le taux courant associé à la valeur d'un point.
1013 Tous les taux de l'année courante sont chargés, pour optimiser un
1014 affichage en liste. (On pourrait probablement améliorer le manager pour
1015 lui greffer le taux courant sous forme de JOIN)
1016 """
1017 for tauxchange in self.tauxchange:
1018 if tauxchange.implantation_id == self.implantation_id:
1019 return tauxchange
1020 return None
1021
9afaa55e 1022 def __unicode__(self):
8c1ae2b3 1023 return u'%s %s (%s)' % (self.valeur, self.devise, self.annee)
6e7c919b
NC
1024
1025
1026class ValeurPoint(ValeurPoint_):
1027 __doc__ = ValeurPoint_.__doc__
1028
e9bbd6ba 1029
d6985a3a 1030class Devise(AUFMetadata):
6e4600ef 1031 """Devise monétaire.
1032 """
e9bbd6ba 1033 code = models.CharField(max_length=10, unique=True)
1034 nom = models.CharField(max_length=255)
1035
6e4600ef 1036 class Meta:
1037 ordering = ['code']
8c1ae2b3 1038 verbose_name = "Devise"
1039 verbose_name_plural = "Devises"
6e4600ef 1040
e9bbd6ba 1041 def __unicode__(self):
1042 return u'%s - %s' % (self.code, self.nom)
1043
d6985a3a 1044class TypeContrat(AUFMetadata):
6e4600ef 1045 """Type de contrat.
1046 """
e9bbd6ba 1047 nom = models.CharField(max_length=255)
6e4600ef 1048 nom_long = models.CharField(max_length=255)
49f9f116 1049
8c1ae2b3 1050 class Meta:
1051 ordering = ['nom']
1052 verbose_name = "Type de contrat"
1053 verbose_name_plural = "Types de contrat"
1054
9afaa55e 1055 def __unicode__(self):
1056 return u'%s' % (self.nom)
30be56d5 1057
2d4d2fcf 1058
1059### AUTRES
1060
d6985a3a 1061class ResponsableImplantation(AUFMetadata):
30be56d5 1062 """Le responsable d'une implantation.
1063 Anciennement géré sur le Dossier du responsable.
1064 """
6e4600ef 1065 employe = models.ForeignKey('Employe', db_column='employe',
1066 related_name='+',
1067 null=True, blank=True)
1068 implantation = models.ForeignKey(ref.Implantation,
1069 db_column='implantation', related_name='+',
1070 unique=True)
30be56d5 1071
1072 def __unicode__(self):
1073 return u'%s : %s' % (self.implantation, self.employe)
1074
1075 class Meta:
1076 ordering = ['implantation__nom']
8c1ae2b3 1077 verbose_name = "Responsable d'implantation"
1078 verbose_name_plural = "Responsables d'implantation"
5ea6b5bb 1079
1080def dossier_piece_dispatch(instance, filename):
1081 path = "dossier/%s/%s" % (instance.dossier_id, filename)
1082 return path