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