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