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