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