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