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