Merge branch 'dev' of ssh://git.auf/auf_rh_dae into dev
[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})
c267f20c 366
367 def postes_encours(self):
368 pass
369
370 def dossiers_passes(self):
371 today = date.today()
372 return self.dossiers.filter(date_fin__lt=today).order_by('-date_fin')
373
374 def dossiers_futurs(self):
375 today = date.today()
376 return self.dossiers.filter(date_debut__gt=today).order_by('-date_fin')
377
378 def dossiers_encours(self):
379 dossiers_p_f = self.dossiers_passes() | self.dossiers_futurs()
380 ids_dossiers_p_f = [d.id for d in dossiers_p_f]
381 return self.dossiers.exclude(id__in=ids_dossiers_p_f).order_by('-date_fin')
9afaa55e 382
7abc6d45 383class EmployePiece(models.Model):
6e4600ef 384 """Documents relatifs à un employé.
7abc6d45 385 Ex.: CV...
386 """
8c1ae2b3 387 employe = models.ForeignKey('Employe', db_column='employe',
6e4600ef 388 related_name='+')
8c1ae2b3 389 nom = models.CharField(verbose_name="Nom", max_length=255)
390 fichier = models.FileField(verbose_name="Fichier",
5ea6b5bb 391 upload_to=employe_piece_dispatch,
7abc6d45 392 storage=storage_prive)
393
6e4600ef 394 class Meta:
395 ordering = ['nom']
396
397 def __unicode__(self):
398 return u'%s' % (self.nom)
399
07b40eda 400class EmployeCommentaire(Commentaire):
8c1ae2b3 401 employe = models.ForeignKey('Employe', db_column='employe',
6e4600ef 402 related_name='+')
9afaa55e 403
2d4d2fcf 404
e9bbd6ba 405LIEN_PARENTE_CHOICES = (
406 ('Conjoint', 'Conjoint'),
407 ('Conjointe', 'Conjointe'),
408 ('Fille', 'Fille'),
409 ('Fils', 'Fils'),
410)
411
d6985a3a 412class AyantDroit(AUFMetadata):
6e4600ef 413 """Personne en relation avec un Employe.
414 """
9afaa55e 415 # Identification
e9bbd6ba 416 nom = models.CharField(max_length=255)
8c1ae2b3 417 prenom = models.CharField(max_length=255,
418 verbose_name="Prénom",)
6e4600ef 419 nom_affichage = models.CharField(max_length=255,
8c1ae2b3 420 verbose_name="Nom d'affichage",
6e4600ef 421 null=True, blank=True)
2d4d2fcf 422 nationalite = models.ForeignKey(ref.Pays, to_field='code',
6e4600ef 423 db_column='nationalite',
8c1ae2b3 424 related_name='ayantdroits_nationalite',
425 verbose_name="Nationalité")
6e4600ef 426 date_naissance = models.DateField(help_text=HELP_TEXT_DATE,
8c1ae2b3 427 verbose_name="Date de naissance",
25037368 428 validators=[validate_date_passee],
6e4600ef 429 null=True, blank=True)
2d4d2fcf 430 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
83b7692b 431
9afaa55e 432 # Relation
433 employe = models.ForeignKey('Employe', db_column='employe',
8c1ae2b3 434 related_name='ayantdroits',
435 verbose_name="Employé")
6e4600ef 436 lien_parente = models.CharField(max_length=10,
437 choices=LIEN_PARENTE_CHOICES,
8c1ae2b3 438 verbose_name="Lien de parenté",
6e4600ef 439 null=True, blank=True)
440
441 class Meta:
442 ordering = ['nom_affichage']
8c1ae2b3 443 verbose_name = "Ayant droit"
444 verbose_name_plural = "Ayants droit"
445
6e4600ef 446 def __unicode__(self):
8c1ae2b3 447 return u'%s' % (self.get_nom())
448
449 def get_nom(self):
450 nom_affichage = self.nom_affichage
451 if not nom_affichage:
452 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
453 return nom_affichage
83b7692b 454
07b40eda 455class AyantDroitCommentaire(Commentaire):
8c1ae2b3 456 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
6e4600ef 457 related_name='+')
83b7692b 458
2d4d2fcf 459
83b7692b 460### DOSSIER
461
462STATUT_RESIDENCE_CHOICES = (
463 ('local', 'Local'),
464 ('expat', 'Expatrié'),
465)
466
467COMPTE_COMPTA_CHOICES = (
468 ('coda', 'CODA'),
469 ('scs', 'SCS'),
470 ('aucun', 'Aucun'),
471)
472
d6985a3a 473class Dossier_(AUFMetadata):
6e4600ef 474 """Le Dossier regroupe les informations relatives à l'occupation
475 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
476 par un Employe.
477
478 Plusieurs Contrats peuvent être associés au Dossier.
479 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
480 lequel aucun Dossier n'existe est un poste vacant.
481 """
83b7692b 482 # Identification
6e4600ef 483 employe = models.ForeignKey('Employe', db_column='employe',
d04d084c 484 related_name='dossiers',
8c1ae2b3 485 verbose_name="Employé")
1f2979b8 486 poste = models.ForeignKey('Poste', db_column='poste', related_name='+')
8277a35b
NC
487 statut = models.ForeignKey('Statut', related_name='+', default=3,
488 null=True)
83b7692b 489 organisme_bstg = models.ForeignKey('OrganismeBstg',
6e4600ef 490 db_column='organisme_bstg',
491 related_name='+',
8c1ae2b3 492 verbose_name="Organisme",
493 help_text="Si détaché (DET) ou \
6e4600ef 494 mis à disposition (MAD), \
495 préciser l'organisme.",
496 null=True, blank=True)
497
83b7692b 498 # Recrutement
2d4d2fcf 499 remplacement = models.BooleanField(default=False)
83b7692b 500 statut_residence = models.CharField(max_length=10, default='local',
8277a35b 501 verbose_name="Statut", null=True,
2d4d2fcf 502 choices=STATUT_RESIDENCE_CHOICES)
83b7692b 503
504 # Rémunération
6e4600ef 505 classement = models.ForeignKey('Classement', db_column='classement',
506 related_name='+',
507 null=True, blank=True)
8277a35b 508 regime_travail = models.DecimalField(max_digits=12, null=True,
2d4d2fcf 509 decimal_places=2,
510 default=REGIME_TRAVAIL_DEFAULT,
8c1ae2b3 511 verbose_name="Régime de travail",
512 help_text="% du temps complet")
83b7692b 513 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
8277a35b 514 decimal_places=2, null=True,
2d4d2fcf 515 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
8c1ae2b3 516 verbose_name="Nb. heures par semaine")
7abc6d45 517
518 # Occupation du Poste par cet Employe (anciennement "mandat")
8c1ae2b3 519 date_debut = models.DateField(verbose_name="Date de début d'occupation \
6e4600ef 520 de poste",
521 help_text=HELP_TEXT_DATE)
8c1ae2b3 522 date_fin = models.DateField(verbose_name="Date de fin d'occupation \
6e4600ef 523 de poste",
2d4d2fcf 524 help_text=HELP_TEXT_DATE,
6e4600ef 525 null=True, blank=True)
e9bbd6ba 526
2d4d2fcf 527 # Comptes
528 # TODO?
83b7692b 529
6e4600ef 530 class Meta:
37868f0b 531 abstract = True
49449367 532 ordering = ['employe__nom', ]
8c1ae2b3 533 verbose_name = "Dossier"
534 verbose_name_plural = "Dossiers"
6e4600ef 535
83b7692b 536 def __unicode__(self):
8c1ae2b3 537 poste = self.poste.nom
538 if self.employe.genre == 'F':
539 poste = self.poste.nom_feminin
540 return u'%s - %s' % (self.employe, poste)
83b7692b 541
37868f0b
NC
542
543class Dossier(Dossier_):
544 __doc__ = Dossier_.__doc__
545
546
83b7692b 547class DossierPiece(models.Model):
7abc6d45 548 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
549 Ex.: Lettre de motivation.
550 """
8c1ae2b3 551 dossier = models.ForeignKey('Dossier', db_column='dossier',
6e4600ef 552 related_name='+')
8c1ae2b3 553 nom = models.CharField(verbose_name="Nom", max_length=255)
554 fichier = models.FileField(verbose_name="Fichier",
83b7692b 555 upload_to=dossier_piece_dispatch,
556 storage=storage_prive)
557
6e4600ef 558 class Meta:
559 ordering = ['nom']
560
561 def __unicode__(self):
562 return u'%s' % (self.nom)
563
07b40eda 564class DossierCommentaire(Commentaire):
8c1ae2b3 565 dossier = models.ForeignKey('Dossier', db_column='dossier',
6e4600ef 566 related_name='+')
83b7692b 567
1d0f4eef
OL
568class DossierComparaison(models.Model):
569 """
570 Photo d'une comparaison salariale au moment de l'embauche.
571 """
572 dossier = models.ForeignKey('Dossier', related_name='comparaisons')
8c6269cc 573 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
1d0f4eef
OL
574 poste = models.CharField(max_length=255, null=True, blank=True)
575 personne = models.CharField(max_length=255, null=True, blank=True)
576 montant = models.IntegerField(null=True)
93494cf1 577 devise = models.ForeignKey('Devise', default=5, related_name='+', null=True, blank=True)
1d0f4eef
OL
578
579 def taux_devise(self):
580 liste_taux = self.devise.tauxchange_set.order_by('-annee').filter(implantation=self.dossier.poste.implantation)
581 if len(liste_taux) == 0:
582 raise Exception(u"La devise %s n'a pas de taux pour l'implantation %s" % (self.devise, self.dossier.poste.implantation))
583 else:
584 return liste_taux[0].taux
585
586 def montant_euros(self):
587 return round(float(self.montant) * float(self.taux_devise()), 2)
588
2d4d2fcf 589
07b40eda 590### RÉMUNÉRATION
e9bbd6ba 591
d6985a3a 592class RemunerationMixin(AUFMetadata):
9afaa55e 593 # Identification
6e7c919b
NC
594 dossier = models.ForeignKey('Dossier', db_column='dossier',
595 related_name='%(app_label)s_%(class)s_remunerations')
83b7692b 596 type = models.ForeignKey('TypeRemuneration', db_column='type',
8c1ae2b3 597 related_name='+',
598 verbose_name="Type de rémunération")
7abc6d45 599 type_revalorisation = models.ForeignKey('TypeRevalorisation',
600 db_column='type_revalorisation',
6e4600ef 601 related_name='+',
8c1ae2b3 602 verbose_name="Type de revalorisation",
7abc6d45 603 null=True, blank=True)
50fa9bc1 604 montant = models.FloatField(null=True, blank=True,
6e4600ef 605 default=0)
2d4d2fcf 606 # Annuel (12 mois, 52 semaines, 364 jours?)
c589d980 607 devise = models.ForeignKey('Devise', to_field='id',
6e4600ef 608 db_column='devise', related_name='+',
609 default=5)
2d4d2fcf 610 # commentaire = precision
611 commentaire = models.CharField(max_length=255, null=True, blank=True)
612 # date_debut = anciennement date_effectif
8c1ae2b3 613 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
614 verbose_name="Date de début",
6e4600ef 615 null=True, blank=True)
616 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
8c1ae2b3 617 verbose_name="Date de fin",
6e4600ef 618 null=True, blank=True)
83b7692b 619
2d4d2fcf 620 class Meta:
621 abstract = True
6e4600ef 622 ordering = ['type__nom', '-date_fin']
623
624 def __unicode__(self):
625 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
2d4d2fcf 626
6e7c919b 627class Remuneration_(RemunerationMixin):
2d4d2fcf 628 """Structure de rémunération (données budgétaires) en situation normale
629 pour un Dossier. Si un Evenement existe, utiliser la structure de
630 rémunération EvenementRemuneration de cet événement.
631 """
83b7692b 632
633 def montant_mois(self):
634 return round(self.montant / 12, 2)
635
636 def taux_devise(self):
637 return self.devise.tauxchange_set.order_by('-annee').all()[0].taux
638
639 def montant_euro(self):
640 return round(float(self.montant) / float(self.taux_devise()), 2)
641
642 def montant_euro_mois(self):
643 return round(self.montant_euro() / 12, 2)
9afaa55e 644
645 def __unicode__(self):
646 try:
647 devise = self.devise.code
648 except:
649 devise = "???"
650 return "%s %s" % (self.montant, devise)
83b7692b 651
6e7c919b
NC
652 class Meta:
653 abstract = True
8c1ae2b3 654 verbose_name = "Rémunération"
655 verbose_name_plural = "Rémunérations"
6e7c919b
NC
656
657
658class Remuneration(Remuneration_):
659 __doc__ = Remuneration_.__doc__
660
2d4d2fcf 661
662### CONTRATS
c41b7fcc
OL
663
664class ContratManager(NoDeleteManager):
665 def get_query_set(self):
666 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
667
2d4d2fcf 668
d6985a3a 669class Contrat(AUFMetadata):
2d4d2fcf 670 """Document juridique qui encadre la relation de travail d'un Employe
671 pour un Poste particulier. Pour un Dossier (qui documente cette
672 relation de travail) plusieurs contrats peuvent être associés.
673 """
c41b7fcc
OL
674
675 objects = ContratManager()
676
6e4600ef 677 dossier = models.ForeignKey('Dossier', db_column='dossier',
678 related_name='+')
679 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
8c1ae2b3 680 related_name='+',
681 verbose_name="Type de contrat")
682 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
683 verbose_name="Date de début")
6e4600ef 684 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
8c1ae2b3 685 verbose_name="Date de fin",
6e4600ef 686 null=True, blank=True)
687
688 class Meta:
689 ordering = ['dossier__employe__nom_affichage']
8c1ae2b3 690 verbose_name = "Contrat"
691 verbose_name_plural = "Contrats"
6e4600ef 692
693 def __unicode__(self):
8c1ae2b3 694 return u'%s - %s' % (self.dossier, self.id)
6e4600ef 695
696# TODO? class ContratPiece(models.Model):
2d4d2fcf 697
698
699### ÉVÉNEMENTS
700
d6985a3a 701class Evenement_(AUFMetadata):
6e4600ef 702 """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
703 d'un Dossier qui vient altérer des informations normales liées à un Dossier
704 (ex.: la Remuneration).
705
706 Ex.: congé de maternité, maladie...
707
708 Lors de ces situations exceptionnelles, l'Employe a un régime de travail
709 différent et une rémunération en conséquence. On souhaite toutefois
710 conserver le Dossier intact afin d'éviter une re-saisie des données lors
711 du retour à la normale.
712 """
8c1ae2b3 713 dossier = models.ForeignKey('Dossier', db_column='dossier',
6e4600ef 714 related_name='+')
715 nom = models.CharField(max_length=255)
8c1ae2b3 716 date_debut = models.DateField(help_text=HELP_TEXT_DATE,
717 verbose_name="Date de début")
718 date_fin = models.DateField(help_text=HELP_TEXT_DATE,
719 verbose_name="Date de fin",
6e4600ef 720 null=True, blank=True)
6e7c919b 721
6e4600ef 722 class Meta:
6e7c919b 723 abstract = True
6e4600ef 724 ordering = ['nom']
8c1ae2b3 725 verbose_name = "Évènement"
726 verbose_name_plural = "Évènements"
6e4600ef 727
728 def __unicode__(self):
729 return u'%s' % (self.nom)
6e7c919b
NC
730
731
732class Evenement(Evenement_):
733 __doc__ = Evenement_.__doc__
734
2d4d2fcf 735
6e7c919b 736class EvenementRemuneration_(RemunerationMixin):
6e4600ef 737 """Structure de rémunération liée à un Evenement qui remplace
738 temporairement la Remuneration normale d'un Dossier, pour toute la durée
739 de l'Evenement.
740 """
741 evenement = models.ForeignKey("Evenement", db_column='evenement',
8c1ae2b3 742 related_name='+',
743 verbose_name="Évènement")
744 # TODO : le champ dossier hérité de Remuneration doit être dérivé
745 # de l'Evenement associé
83b7692b 746
6e7c919b
NC
747 class Meta:
748 abstract = True
8c1ae2b3 749 ordering = ['evenement', 'type__nom', '-date_fin']
750 verbose_name = "Évènement - rémunération"
751 verbose_name_plural = "Évènements - rémunérations"
6e7c919b
NC
752
753
754class EvenementRemuneration(EvenementRemuneration_):
755 __doc__ = EvenementRemuneration_.__doc__
83b7692b 756
f31ddfa0
NC
757 class Meta:
758 abstract = True
759
760
761class EvenementRemuneration(EvenementRemuneration_):
762 __doc__ = EvenementRemuneration_.__doc__
763
83b7692b 764
765### RÉFÉRENCES RH
766
d6985a3a 767class FamilleEmploi(AUFMetadata):
6e4600ef 768 """Catégorie utilisée dans la gestion des Postes.
769 Catégorie supérieure à TypePoste.
770 """
e9bbd6ba 771 nom = models.CharField(max_length=255)
6e4600ef 772
8c1ae2b3 773 class Meta:
774 ordering = ['nom']
775 verbose_name = "Famille d'emploi"
776 verbose_name_plural = "Familles d'emploi"
777
6e4600ef 778 def __unicode__(self):
779 return u'%s' % (self.nom)
e9bbd6ba 780
d6985a3a 781class TypePoste(AUFMetadata):
6e4600ef 782 """Catégorie de Poste.
783 """
e9bbd6ba 784 nom = models.CharField(max_length=255)
8c1ae2b3 785 nom_feminin = models.CharField(max_length=255,
786 verbose_name="Nom féminin")
6e4600ef 787
8c1ae2b3 788 is_responsable = models.BooleanField(default=False,
789 verbose_name="Poste de responsabilité")
9afaa55e 790 famille_emploi = models.ForeignKey('FamilleEmploi',
6e4600ef 791 db_column='famille_emploi',
8c1ae2b3 792 related_name='+',
793 verbose_name="Famille d'emploi")
e9bbd6ba 794
6e4600ef 795 class Meta:
796 ordering = ['nom']
8c1ae2b3 797 verbose_name = "Type de poste"
798 verbose_name_plural = "Types de poste"
6e4600ef 799
e9bbd6ba 800 def __unicode__(self):
6e4600ef 801 return u'%s' % (self.nom)
e9bbd6ba 802
803
804TYPE_PAIEMENT_CHOICES = (
805 ('Régulier', 'Régulier'),
806 ('Ponctuel', 'Ponctuel'),
807)
808
809NATURE_REMUNERATION_CHOICES = (
810 ('Accessoire', 'Accessoire'),
811 ('Charges', 'Charges'),
812 ('Indemnité', 'Indemnité'),
7abc6d45 813 ('RAS', 'Rémunération autre source'),
e9bbd6ba 814 ('Traitement', 'Traitement'),
815)
816
d6985a3a 817class TypeRemuneration(AUFMetadata):
6e4600ef 818 """Catégorie de Remuneration.
819 """
e9bbd6ba 820 nom = models.CharField(max_length=255)
9afaa55e 821 type_paiement = models.CharField(max_length=30,
8c1ae2b3 822 choices=TYPE_PAIEMENT_CHOICES,
823 verbose_name="Type de paiement")
9afaa55e 824 nature_remuneration = models.CharField(max_length=30,
8c1ae2b3 825 choices=NATURE_REMUNERATION_CHOICES,
826 verbose_name="Nature de la rémunération")
827
828 class Meta:
829 ordering = ['nom']
830 verbose_name = "Type de rémunération"
831 verbose_name_plural = "Types de rémunération"
9afaa55e 832
833 def __unicode__(self):
6e4600ef 834 return u'%s' % (self.nom)
e9bbd6ba 835
d6985a3a 836class TypeRevalorisation(AUFMetadata):
7abc6d45 837 """Justification du changement de la Remuneration.
6e4600ef 838 (Actuellement utilisé dans aucun traitement informatique.)
7abc6d45 839 """
e9bbd6ba 840 nom = models.CharField(max_length=255)
8c1ae2b3 841
842 class Meta:
843 ordering = ['nom']
844 verbose_name = "Type de revalorisation"
845 verbose_name_plural = "Types de revalorisation"
e9bbd6ba 846
847 def __unicode__(self):
6e4600ef 848 return u'%s' % (self.nom)
849
d6985a3a 850class Service(AUFMetadata):
6e4600ef 851 """Unité administrative où les Postes sont rattachés.
852 """
853 nom = models.CharField(max_length=255)
9afaa55e 854
855 class Meta:
856 ordering = ['nom']
8c1ae2b3 857 verbose_name = "Service"
858 verbose_name_plural = "Services"
e9bbd6ba 859
6e4600ef 860 def __unicode__(self):
861 return u'%s' % (self.nom)
862
e9bbd6ba 863
864TYPE_ORGANISME_CHOICES = (
865 ('MAD', 'Mise à disposition'),
866 ('DET', 'Détachement'),
867)
868
d6985a3a 869class OrganismeBstg(AUFMetadata):
6e4600ef 870 """Organisation d'où provient un Employe mis à disposition (MAD) de
871 ou détaché (DET) à l'AUF à titre gratuit.
872
873 (BSTG = bien et service à titre gratuit.)
874 """
e9bbd6ba 875 nom = models.CharField(max_length=255)
876 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
6e4600ef 877 pays = models.ForeignKey(ref.Pays, to_field='code',
878 db_column='pays',
879 related_name='organismes_bstg',
880 null=True, blank=True)
9afaa55e 881
882 class Meta:
883 ordering = ['type', 'nom']
8c1ae2b3 884 verbose_name = "Organisme BSTG"
885 verbose_name_plural = "Organismes BSTG"
9afaa55e 886
6e4600ef 887 def __unicode__(self):
8c1ae2b3 888 return u'%s (%s)' % (self.nom, self.get_type_display())
83b7692b 889
d6985a3a 890class Statut(AUFMetadata):
6e4600ef 891 """Statut de l'Employe dans le cadre d'un Dossier particulier.
892 """
9afaa55e 893 # Identification
e9bbd6ba 894 code = models.CharField(max_length=25, unique=True)
895 nom = models.CharField(max_length=255)
e9bbd6ba 896
6e4600ef 897 class Meta:
898 ordering = ['code']
8c1ae2b3 899 verbose_name = "Statut d'employé"
900 verbose_name_plural = "Statuts d'employé"
6e4600ef 901
9afaa55e 902 def __unicode__(self):
903 return u'%s : %s' % (self.code, self.nom)
904
83b7692b 905
e9bbd6ba 906TYPE_CLASSEMENT_CHOICES = (
6e4600ef 907 ('S', 'S -Soutien'),
908 ('T', 'T - Technicien'),
909 ('P', 'P - Professionel'),
910 ('C', 'C - Cadre'),
911 ('D', 'D - Direction'),
912 ('SO', 'SO - Sans objet [expatriés]'),
913 ('HG', 'HG - Hors grille [direction]'),
e9bbd6ba 914)
83b7692b 915
6e7c919b 916
d6985a3a 917class Classement_(AUFMetadata):
6e4600ef 918 """Éléments de classement de la
919 "Grille générique de classement hiérarchique".
920
921 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
922 classement dans la grille. Le classement donne le coefficient utilisé dans:
923
924 salaire de base = coefficient * valeur du point de l'Implantation du Poste
925 """
9afaa55e 926 # Identification
e9bbd6ba 927 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
8c1ae2b3 928 echelon = models.IntegerField(verbose_name="Échelon")
929 degre = models.IntegerField(verbose_name="Degré")
8277a35b
NC
930 coefficient = models.FloatField(default=0, verbose_name="Coéfficient",
931 null=True)
9afaa55e 932 # Méta
6e4600ef 933 # annee # au lieu de date_debut et date_fin
934 commentaire = models.TextField(null=True, blank=True)
935
936 class Meta:
6e7c919b 937 abstract = True
6e4600ef 938 ordering = ['type','echelon','degre','coefficient']
8c1ae2b3 939 verbose_name = "Classement"
940 verbose_name_plural = "Classements"
e9bbd6ba 941
942 def __unicode__(self):
943 return u'%s.%s.%s (%s)' % (self.type, self.echelon, self.degre,
944 self.coefficient)
945
6e7c919b
NC
946class Classement(Classement_):
947 __doc__ = Classement_.__doc__
948
949
d6985a3a 950class TauxChange_(AUFMetadata):
7abc6d45 951 """Taux de change de la devise vers l'euro (EUR)
6e4600ef 952 pour chaque année budgétaire.
7abc6d45 953 """
9afaa55e 954 # Identification
8c1ae2b3 955 devise = models.ForeignKey('Devise', db_column='devise',
6e4600ef 956 related_name='+')
8c1ae2b3 957 annee = models.IntegerField(verbose_name="Année")
958 taux = models.FloatField(verbose_name="Taux vers l'euro")
6e7c919b 959
6e4600ef 960 class Meta:
6e7c919b 961 abstract = True
8c1ae2b3 962 ordering = ['-annee', 'devise__code']
963 verbose_name = "Taux de change"
964 verbose_name_plural = "Taux de change"
6e4600ef 965
966 def __unicode__(self):
8c1ae2b3 967 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
e9bbd6ba 968
6e7c919b
NC
969
970class TauxChange(TauxChange_):
971 __doc__ = TauxChange_.__doc__
972
701f3bea
OL
973class ValeurPointManager(NoDeleteManager):
974 def get_query_set(self):
975 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
976
6e7c919b 977
d6985a3a 978class ValeurPoint_(AUFMetadata):
6e4600ef 979 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
980 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
8c1ae2b3 981 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
6e4600ef 982
983 salaire de base = coefficient * valeur du point de l'Implantation du Poste
984 """
701f3bea
OL
985
986 objects = ValeurPointManager()
987
8277a35b
NC
988 valeur = models.FloatField(null=True)
989 devise = models.ForeignKey('Devise', db_column='devise', null=True,
6e4600ef 990 related_name='+', default=5)
83b7692b 991 implantation = models.ForeignKey(ref.Implantation,
6e7c919b
NC
992 db_column='implantation',
993 related_name='%(app_label)s_valeur_point')
9afaa55e 994 # Méta
e9bbd6ba 995 annee = models.IntegerField()
9afaa55e 996
6e4600ef 997 class Meta:
701f3bea 998 ordering = ['-annee', 'implantation__nom']
6e7c919b 999 abstract = True
8c1ae2b3 1000 verbose_name = "Valeur du point"
1001 verbose_name_plural = "Valeurs du point"
6e0bbb73 1002
e9d7483c 1003 # TODO : cette fonction n'était pas présente dans la branche dev, utilité?
ee23ecbc
NC
1004 def get_tauxchange_courant(self):
1005 """
1006 Recherche le taux courant associé à la valeur d'un point.
1007 Tous les taux de l'année courante sont chargés, pour optimiser un
1008 affichage en liste. (On pourrait probablement améliorer le manager pour
1009 lui greffer le taux courant sous forme de JOIN)
1010 """
1011 for tauxchange in self.tauxchange:
1012 if tauxchange.implantation_id == self.implantation_id:
1013 return tauxchange
1014 return None
1015
9afaa55e 1016 def __unicode__(self):
8c1ae2b3 1017 return u'%s %s (%s)' % (self.valeur, self.devise, self.annee)
6e7c919b
NC
1018
1019
1020class ValeurPoint(ValeurPoint_):
1021 __doc__ = ValeurPoint_.__doc__
1022
e9bbd6ba 1023
d6985a3a 1024class Devise(AUFMetadata):
6e4600ef 1025 """Devise monétaire.
1026 """
e9bbd6ba 1027 code = models.CharField(max_length=10, unique=True)
1028 nom = models.CharField(max_length=255)
1029
6e4600ef 1030 class Meta:
1031 ordering = ['code']
8c1ae2b3 1032 verbose_name = "Devise"
1033 verbose_name_plural = "Devises"
6e4600ef 1034
e9bbd6ba 1035 def __unicode__(self):
1036 return u'%s - %s' % (self.code, self.nom)
1037
d6985a3a 1038class TypeContrat(AUFMetadata):
6e4600ef 1039 """Type de contrat.
1040 """
e9bbd6ba 1041 nom = models.CharField(max_length=255)
6e4600ef 1042 nom_long = models.CharField(max_length=255)
49f9f116 1043
8c1ae2b3 1044 class Meta:
1045 ordering = ['nom']
1046 verbose_name = "Type de contrat"
1047 verbose_name_plural = "Types de contrat"
1048
9afaa55e 1049 def __unicode__(self):
1050 return u'%s' % (self.nom)
30be56d5 1051
2d4d2fcf 1052
1053### AUTRES
1054
d6985a3a 1055class ResponsableImplantation(AUFMetadata):
30be56d5 1056 """Le responsable d'une implantation.
1057 Anciennement géré sur le Dossier du responsable.
1058 """
6e4600ef 1059 employe = models.ForeignKey('Employe', db_column='employe',
1060 related_name='+',
1061 null=True, blank=True)
1062 implantation = models.ForeignKey(ref.Implantation,
1063 db_column='implantation', related_name='+',
1064 unique=True)
30be56d5 1065
1066 def __unicode__(self):
1067 return u'%s : %s' % (self.implantation, self.employe)
1068
1069 class Meta:
1070 ordering = ['implantation__nom']
8c1ae2b3 1071 verbose_name = "Responsable d'implantation"
1072 verbose_name_plural = "Responsables d'implantation"
5ea6b5bb 1073
1074def dossier_piece_dispatch(instance, filename):
1075 path = "dossier/%s/%s" % (instance.dossier_id, filename)
1076 return path