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