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 help_text=HELP_TEXT_DATE,
344 null=True, blank=True)
345
346 # Coordonnées
347 tel_domicile = models.CharField(max_length=255,
348 verbose_name = u"Tél. domicile",
349 null=True, blank=True)
350 tel_cellulaire = models.CharField(max_length=255,
351 verbose_name = u"Tél. cellulaire",
352 null=True, blank=True)
353 adresse = models.CharField(max_length=255, null=True, blank=True)
354 ville = models.CharField(max_length=255, null=True, blank=True)
355 province = models.CharField(max_length=255, null=True, blank=True)
356 code_postal = models.CharField(max_length=255, null=True, blank=True)
357 pays = models.ForeignKey(ref.Pays, to_field='code', db_column='pays',
358 related_name='employes',
359 null=True, blank=True)
360
361 class Meta:
362 ordering = ['nom_affichage','nom','prenom']
363 verbose_name = u"Employé"
364 verbose_name_plural = u"Employés"
365
366 def __unicode__(self):
367 return u'%s [%s]' % (self.get_nom(), self.id)
368
369 def get_nom(self):
370 nom_affichage = self.nom_affichage
371 if not nom_affichage:
372 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
373 return nom_affichage
374
375 def civilite(self):
376 civilite = u''
377 if self.genre.upper() == u'M':
378 civilite = u'M.'
379 elif self.genre.upper() == u'F':
380 civilite = u'Mme'
381 return civilite
382
383 def url_photo(self):
384 """Retourne l'URL du service retournant la photo de l'Employe.
385 Équivalent reverse url 'rh_photo' avec id en param.
386 """
387 from django.core.urlresolvers import reverse
388 return reverse('rh_photo', kwargs={'id':self.id})
389
390 def dossiers_passes(self):
391 today = date.today()
392 dossiers_passes = self.dossiers.filter(date_fin__lt=today).order_by('-date_fin')
393 for d in dossiers_passes:
394 d.archive = True
395 return dossiers_passes
396
397 def dossiers_futurs(self):
398 today = date.today()
399 return self.dossiers.filter(date_debut__gt=today).order_by('-date_fin')
400
401 def dossiers_encours(self):
402 dossiers_p_f = self.dossiers_passes() | self.dossiers_futurs()
403 ids_dossiers_p_f = [d.id for d in dossiers_p_f]
404 dossiers_encours = self.dossiers.exclude(id__in=ids_dossiers_p_f).order_by('-date_fin')
405
406 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
407 for d in dossiers_encours:
408 d.remunerations = Remuneration.objects.filter(dossier=d.id).order_by('-id')
409 return dossiers_encours
410
411 def postes_encours(self):
412 postes_encours = set()
413 for d in self.dossiers_encours():
414 postes_encours.add(d.poste)
415 return postes_encours
416
417 def poste_principal(self):
418 """
419 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
420 Idée derrière :
421 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
422 """
423 poste = Poste.objects.none()
424 try:
425 poste = self.dossiers_encours().order_by('date_debut')[0].poste
426 except:
427 pass
428 return poste
429
430 prefix_implantation = "dossiers__poste__implantation__region"
431 def get_regions(self):
432 regions = []
433 for d in self.dossiers.all():
434 regions.append(d.poste.implantation.region)
435 return regions
436
437
438 class EmployePiece(models.Model):
439 """Documents relatifs à un employé.
440 Ex.: CV...
441 """
442 employe = models.ForeignKey('Employe', db_column='employe')
443 nom = models.CharField(verbose_name="Nom", max_length=255)
444 fichier = models.FileField(verbose_name="Fichier",
445 upload_to=employe_piece_dispatch,
446 storage=storage_prive)
447
448 class Meta:
449 ordering = ['nom']
450 verbose_name = u"Employé pièce"
451 verbose_name_plural = u"Employé pièces"
452
453 def __unicode__(self):
454 return u'%s' % (self.nom)
455
456 class EmployeCommentaire(Commentaire):
457 employe = models.ForeignKey('Employe', db_column='employe',
458 related_name='+')
459
460 class Meta:
461 verbose_name = u"Employé commentaire"
462 verbose_name_plural = u"Employé commentaires"
463
464
465 LIEN_PARENTE_CHOICES = (
466 ('Conjoint', 'Conjoint'),
467 ('Conjointe', 'Conjointe'),
468 ('Fille', 'Fille'),
469 ('Fils', 'Fils'),
470 )
471
472 class AyantDroit(AUFMetadata):
473 """Personne en relation avec un Employe.
474 """
475 # Identification
476 nom = models.CharField(max_length=255)
477 prenom = models.CharField(max_length=255,
478 verbose_name = u"Prénom",)
479 nom_affichage = models.CharField(max_length=255,
480 verbose_name = u"Nom d'affichage",
481 null=True, blank=True)
482 nationalite = models.ForeignKey(ref.Pays, to_field='code',
483 db_column='nationalite',
484 related_name='ayantdroits_nationalite',
485 verbose_name = u"Nationalité",
486 null=True, blank=True)
487 date_naissance = models.DateField(verbose_name = u"Date de naissance",
488 validators=[validate_date_passee],
489 null=True, blank=True)
490 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
491
492 # Relation
493 employe = models.ForeignKey('Employe', db_column='employe',
494 related_name='ayantdroits',
495 verbose_name = u"Employé")
496 lien_parente = models.CharField(max_length=10,
497 choices=LIEN_PARENTE_CHOICES,
498 verbose_name = u"Lien de parenté",
499 null=True, blank=True)
500
501 class Meta:
502 ordering = ['nom_affichage']
503 verbose_name = u"Ayant droit"
504 verbose_name_plural = u"Ayants droit"
505
506 def __unicode__(self):
507 return u'%s' % (self.get_nom())
508
509 def get_nom(self):
510 nom_affichage = self.nom_affichage
511 if not nom_affichage:
512 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
513 return nom_affichage
514
515 prefix_implantation = "employe__dossiers__poste__implantation__region"
516 def get_regions(self):
517 regions = []
518 for d in self.employe.dossiers.all():
519 regions.append(d.poste.implantation.region)
520 return regions
521
522
523 class AyantDroitCommentaire(Commentaire):
524 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
525 related_name='+')
526
527
528 ### DOSSIER
529
530 STATUT_RESIDENCE_CHOICES = (
531 ('local', 'Local'),
532 ('expat', 'Expatrié'),
533 )
534
535 COMPTE_COMPTA_CHOICES = (
536 ('coda', 'CODA'),
537 ('scs', 'SCS'),
538 ('aucun', 'Aucun'),
539 )
540
541 class Dossier_(AUFMetadata):
542 """Le Dossier regroupe les informations relatives à l'occupation
543 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
544 par un Employe.
545
546 Plusieurs Contrats peuvent être associés au Dossier.
547 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
548 lequel aucun Dossier n'existe est un poste vacant.
549 """
550
551 objects = DossierManager()
552
553 # TODO: OneToOne ??
554 statut = models.ForeignKey('Statut', related_name='+', default=3,
555 null=True)
556 organisme_bstg = models.ForeignKey('OrganismeBstg',
557 db_column='organisme_bstg',
558 related_name='+',
559 verbose_name = u"Organisme",
560 help_text="Si détaché (DET) ou \
561 mis à disposition (MAD), \
562 préciser l'organisme.",
563 null=True, blank=True)
564
565 # Recrutement
566 remplacement = models.BooleanField(default=False)
567 remplacement_de = models.ForeignKey('self', related_name='+',
568 null=True, blank=True)
569 statut_residence = models.CharField(max_length=10, default='local',
570 verbose_name = u"Statut", null=True,
571 choices=STATUT_RESIDENCE_CHOICES)
572
573 # Rémunération
574 classement = models.ForeignKey('Classement', db_column='classement',
575 related_name='+',
576 null=True, blank=True)
577 regime_travail = models.DecimalField(max_digits=12, null=True,
578 decimal_places=2,
579 default=REGIME_TRAVAIL_DEFAULT,
580 verbose_name = u"Régime de travail",
581 help_text="% du temps complet")
582 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
583 decimal_places=2, null=True,
584 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
585 verbose_name = u"Nb. heures par semaine")
586
587 # Occupation du Poste par cet Employe (anciennement "mandat")
588 date_debut = models.DateField(verbose_name = u"Date de début d'occupation \
589 de poste",)
590 date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \
591 de poste",
592 null=True, blank=True)
593
594 # Comptes
595 # TODO?
596
597 class Meta:
598 abstract = True
599 ordering = ['employe__nom', ]
600 verbose_name = u"Dossier"
601 verbose_name_plural = "Dossiers"
602
603 def salaire_theorique(self):
604 annee = date.today().year
605 coeff = self.classement.coefficient
606 implantation = self.poste.implantation
607 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
608
609 montant = coeff * point.valeur
610 devise = point.devise
611 return {'montant':montant, 'devise':devise}
612
613 def __unicode__(self):
614 poste = self.poste.nom
615 if self.employe.genre == 'F':
616 poste = self.poste.nom_feminin
617 return u'%s - %s' % (self.employe, poste)
618
619 prefix_implantation = "poste__implantation__region"
620 def get_regions(self):
621 return [self.poste.implantation.region]
622
623
624 def remunerations(self):
625 return self.rh_remunerations.all().order_by('date_debut')
626
627 def get_salaire(self):
628 try:
629 return [r for r in self.remunerations().order_by('-date_debut') if r.type_id == 1][0]
630 except:
631 return None
632
633 class Dossier(Dossier_):
634 __doc__ = Dossier_.__doc__
635 poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='%(app_label)s_dossiers')
636 employe = models.ForeignKey('Employe', db_column='employe',
637 related_name='%(app_label)s_dossiers',
638 verbose_name=u"Employé")
639
640
641 class DossierPiece_(models.Model):
642 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
643 Ex.: Lettre de motivation.
644 """
645 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+')
646 nom = models.CharField(verbose_name = u"Nom", max_length=255)
647 fichier = models.FileField(verbose_name = u"Fichier",
648 upload_to=dossier_piece_dispatch,
649 storage=storage_prive)
650
651 class Meta:
652 abstract = True
653 ordering = ['nom']
654
655 def __unicode__(self):
656 return u'%s' % (self.nom)
657
658 class DossierPiece(DossierPiece_):
659 pass
660
661 class DossierCommentaire_(Commentaire):
662 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+')
663 class Meta:
664 abstract = True
665
666 class DossierCommentaire(DossierCommentaire_):
667 pass
668
669 class DossierComparaison_(models.Model):
670 """
671 Photo d'une comparaison salariale au moment de l'embauche.
672 """
673 dossier = models.ForeignKey('%s.Dossier' % app_context(), related_name='%(app_label)s_comparaisons')
674 objects = DossierComparaisonManager()
675
676 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
677 poste = models.CharField(max_length=255, null=True, blank=True)
678 personne = models.CharField(max_length=255, null=True, blank=True)
679 montant = models.IntegerField(null=True)
680 devise = models.ForeignKey('Devise', default=5, related_name='+', null=True, blank=True)
681
682 class Meta:
683 abstract = True
684
685 def taux_devise(self):
686 annee = self.dossier.poste.date_debut.year
687 taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise, annee=annee)]
688 taux = set(taux)
689 if len(taux) != 1:
690 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))
691 else:
692 return list(taux)[0]
693
694 def montant_euros(self):
695 return round(float(self.montant) * float(self.taux_devise()), 2)
696
697 class DossierComparaison(DossierComparaison_):
698 pass
699
700 ### RÉMUNÉRATION
701
702 class RemunerationMixin(AUFMetadata):
703 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_remunerations')
704 # Identification
705 type = models.ForeignKey('TypeRemuneration', db_column='type',
706 related_name='+',
707 verbose_name = u"Type de rémunération")
708 type_revalorisation = models.ForeignKey('TypeRevalorisation',
709 db_column='type_revalorisation',
710 related_name='+',
711 verbose_name = u"Type de revalorisation",
712 null=True, blank=True)
713 montant = models.FloatField(null=True, blank=True,
714 default=0)
715 # Annuel (12 mois, 52 semaines, 364 jours?)
716 devise = models.ForeignKey('Devise', db_column='devise', related_name='+', default=5)
717 # commentaire = precision
718 commentaire = models.CharField(max_length=255, null=True, blank=True)
719 # date_debut = anciennement date_effectif
720 date_debut = models.DateField(verbose_name = u"Date de début",
721 null=True, blank=True)
722 date_fin = models.DateField(verbose_name = u"Date de fin",
723 null=True, blank=True)
724
725 class Meta:
726 abstract = True
727 ordering = ['type__nom', '-date_fin']
728
729 def __unicode__(self):
730 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
731
732 class Remuneration_(RemunerationMixin):
733 """Structure de rémunération (données budgétaires) en situation normale
734 pour un Dossier. Si un Evenement existe, utiliser la structure de
735 rémunération EvenementRemuneration de cet événement.
736 """
737
738 def montant_mois(self):
739 return round(self.montant / 12, 2)
740
741 def taux_devise(self):
742 if self.devise.code == "EUR":
743 return 1
744
745 annee = datetime.datetime.now().year
746 if self.date_debut is not None:
747 annee = self.date_debut.year
748 if self.dossier.poste.date_debut is not None:
749 annee = self.dossier.poste.date_debut.year
750
751 taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise_id, annee=annee)]
752 taux = set(taux)
753 if len(taux) != 1:
754 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))
755 else:
756 return list(taux)[0]
757
758 def montant_euro(self):
759 return round(float(self.montant) * float(self.taux_devise()), 2)
760
761 def montant_euro_mois(self):
762 return round(self.montant_euro() / 12, 2)
763
764 def __unicode__(self):
765 try:
766 devise = self.devise.code
767 except:
768 devise = "???"
769 return "%s %s" % (self.montant, devise)
770
771 class Meta:
772 abstract = True
773 verbose_name = u"Rémunération"
774 verbose_name_plural = u"Rémunérations"
775
776
777 class Remuneration(Remuneration_):
778 pass
779
780
781 ### CONTRATS
782
783 class ContratManager(NoDeleteManager):
784 def get_query_set(self):
785 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
786
787
788 class Contrat_(AUFMetadata):
789 """Document juridique qui encadre la relation de travail d'un Employe
790 pour un Poste particulier. Pour un Dossier (qui documente cette
791 relation de travail) plusieurs contrats peuvent être associés.
792 """
793 objects = ContratManager()
794 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_contrats')
795 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
796 related_name='+',
797 verbose_name = u"Type de contrat")
798 date_debut = models.DateField(verbose_name = u"Date de début")
799 date_fin = models.DateField(verbose_name = u"Date de fin",
800 null=True, blank=True)
801
802 class Meta:
803 abstract = True
804 ordering = ['dossier__employe__nom_affichage']
805 verbose_name = u"Contrat"
806 verbose_name_plural = u"Contrats"
807
808 def __unicode__(self):
809 return u'%s - %s' % (self.dossier, self.id)
810
811 class Contrat(Contrat_):
812 pass
813
814
815 ### ÉVÉNEMENTS
816
817 #class Evenement_(AUFMetadata):
818 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
819 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
820 # (ex.: la Remuneration).
821 #
822 # Ex.: congé de maternité, maladie...
823 #
824 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
825 # différent et une rémunération en conséquence. On souhaite toutefois
826 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
827 # du retour à la normale.
828 # """
829 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
830 # related_name='+')
831 # nom = models.CharField(max_length=255)
832 # date_debut = models.DateField(verbose_name = u"Date de début")
833 # date_fin = models.DateField(verbose_name = u"Date de fin",
834 # null=True, blank=True)
835 #
836 # class Meta:
837 # abstract = True
838 # ordering = ['nom']
839 # verbose_name = u"Évènement"
840 # verbose_name_plural = u"Évènements"
841 #
842 # def __unicode__(self):
843 # return u'%s' % (self.nom)
844 #
845 #
846 #class Evenement(Evenement_):
847 # __doc__ = Evenement_.__doc__
848 #
849 #
850 #class EvenementRemuneration_(RemunerationMixin):
851 # """Structure de rémunération liée à un Evenement qui remplace
852 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
853 # de l'Evenement.
854 # """
855 # evenement = models.ForeignKey("Evenement", db_column='evenement',
856 # related_name='+',
857 # verbose_name = u"Évènement")
858 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
859 # # de l'Evenement associé
860 #
861 # class Meta:
862 # abstract = True
863 # ordering = ['evenement', 'type__nom', '-date_fin']
864 # verbose_name = u"Évènement - rémunération"
865 # verbose_name_plural = u"Évènements - rémunérations"
866 #
867 #
868 #class EvenementRemuneration(EvenementRemuneration_):
869 # __doc__ = EvenementRemuneration_.__doc__
870 #
871 # class Meta:
872 # abstract = True
873 #
874 #
875 #class EvenementRemuneration(EvenementRemuneration_):
876 # __doc__ = EvenementRemuneration_.__doc__
877 # TODO? class ContratPiece(models.Model):
878
879
880 ### RÉFÉRENCES RH
881
882 class FamilleEmploi(AUFMetadata):
883 """Catégorie utilisée dans la gestion des Postes.
884 Catégorie supérieure à TypePoste.
885 """
886 nom = models.CharField(max_length=255)
887
888 class Meta:
889 ordering = ['nom']
890 verbose_name = u"Famille d'emploi"
891 verbose_name_plural = u"Familles d'emploi"
892
893 def __unicode__(self):
894 return u'%s' % (self.nom)
895
896 class TypePoste(AUFMetadata):
897 """Catégorie de Poste.
898 """
899 nom = models.CharField(max_length=255)
900 nom_feminin = models.CharField(max_length=255,
901 verbose_name = u"Nom féminin")
902
903 is_responsable = models.BooleanField(default=False,
904 verbose_name = u"Poste de responsabilité")
905 famille_emploi = models.ForeignKey('FamilleEmploi',
906 db_column='famille_emploi',
907 related_name='+',
908 verbose_name = u"Famille d'emploi")
909
910 class Meta:
911 ordering = ['nom']
912 verbose_name = u"Type de poste"
913 verbose_name_plural = u"Types de poste"
914
915 def __unicode__(self):
916 return u'%s' % (self.nom)
917
918
919 TYPE_PAIEMENT_CHOICES = (
920 ('Régulier', 'Régulier'),
921 ('Ponctuel', 'Ponctuel'),
922 )
923
924 NATURE_REMUNERATION_CHOICES = (
925 ('Accessoire', 'Accessoire'),
926 ('Charges', 'Charges'),
927 ('Indemnité', 'Indemnité'),
928 ('RAS', 'Rémunération autre source'),
929 ('Traitement', 'Traitement'),
930 )
931
932 class TypeRemuneration(AUFMetadata):
933 """Catégorie de Remuneration.
934 """
935 nom = models.CharField(max_length=255)
936 type_paiement = models.CharField(max_length=30,
937 choices=TYPE_PAIEMENT_CHOICES,
938 verbose_name = u"Type de paiement")
939 nature_remuneration = models.CharField(max_length=30,
940 choices=NATURE_REMUNERATION_CHOICES,
941 verbose_name = u"Nature de la rémunération")
942
943 class Meta:
944 ordering = ['nom']
945 verbose_name = u"Type de rémunération"
946 verbose_name_plural = u"Types de rémunération"
947
948 def __unicode__(self):
949 return u'%s' % (self.nom)
950
951 class TypeRevalorisation(AUFMetadata):
952 """Justification du changement de la Remuneration.
953 (Actuellement utilisé dans aucun traitement informatique.)
954 """
955 nom = models.CharField(max_length=255)
956
957 class Meta:
958 ordering = ['nom']
959 verbose_name = u"Type de revalorisation"
960 verbose_name_plural = u"Types de revalorisation"
961
962 def __unicode__(self):
963 return u'%s' % (self.nom)
964
965 class Service(AUFMetadata):
966 """Unité administrative où les Postes sont rattachés.
967 """
968 nom = models.CharField(max_length=255)
969
970 class Meta:
971 ordering = ['nom']
972 verbose_name = u"Service"
973 verbose_name_plural = u"Services"
974
975 def __unicode__(self):
976 return u'%s' % (self.nom)
977
978
979 TYPE_ORGANISME_CHOICES = (
980 ('MAD', 'Mise à disposition'),
981 ('DET', 'Détachement'),
982 )
983
984 class OrganismeBstg(AUFMetadata):
985 """Organisation d'où provient un Employe mis à disposition (MAD) de
986 ou détaché (DET) à l'AUF à titre gratuit.
987
988 (BSTG = bien et service à titre gratuit.)
989 """
990 nom = models.CharField(max_length=255)
991 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
992 pays = models.ForeignKey(ref.Pays, to_field='code',
993 db_column='pays',
994 related_name='organismes_bstg',
995 null=True, blank=True)
996
997 class Meta:
998 ordering = ['type', 'nom']
999 verbose_name = u"Organisme BSTG"
1000 verbose_name_plural = u"Organismes BSTG"
1001
1002 def __unicode__(self):
1003 return u'%s (%s)' % (self.nom, self.get_type_display())
1004
1005 prefix_implantation = "pays__region"
1006 def get_regions(self):
1007 return [self.pays.region]
1008
1009
1010 class Statut(AUFMetadata):
1011 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1012 """
1013 # Identification
1014 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.")
1015 nom = models.CharField(max_length=255)
1016
1017 class Meta:
1018 ordering = ['code']
1019 verbose_name = u"Statut d'employé"
1020 verbose_name_plural = u"Statuts d'employé"
1021
1022 def __unicode__(self):
1023 return u'%s : %s' % (self.code, self.nom)
1024
1025
1026 TYPE_CLASSEMENT_CHOICES = (
1027 ('S', 'S -Soutien'),
1028 ('T', 'T - Technicien'),
1029 ('P', 'P - Professionel'),
1030 ('C', 'C - Cadre'),
1031 ('D', 'D - Direction'),
1032 ('SO', 'SO - Sans objet [expatriés]'),
1033 ('HG', 'HG - Hors grille [direction]'),
1034 )
1035
1036
1037 class Classement_(AUFMetadata):
1038 """Éléments de classement de la
1039 "Grille générique de classement hiérarchique".
1040
1041 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1042 classement dans la grille. Le classement donne le coefficient utilisé dans:
1043
1044 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1045 """
1046 # Identification
1047 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1048 echelon = models.IntegerField(verbose_name=u"Échelon", blank=True, default=0)
1049 degre = models.IntegerField(verbose_name=u"Degré", blank=True, default=0)
1050 coefficient = models.FloatField(default=0, verbose_name=u"Coefficient",
1051 null=True)
1052 # Méta
1053 # annee # au lieu de date_debut et date_fin
1054 commentaire = models.TextField(null=True, blank=True)
1055
1056 class Meta:
1057 abstract = True
1058 ordering = ['type','echelon','degre','coefficient']
1059 verbose_name = u"Classement"
1060 verbose_name_plural = u"Classements"
1061
1062 def __unicode__(self):
1063 return u'%s.%s.%s (%s)' % (self.type, self.echelon, self.degre,
1064 self.coefficient)
1065
1066 class Classement(Classement_):
1067 __doc__ = Classement_.__doc__
1068
1069
1070 class TauxChange_(AUFMetadata):
1071 """Taux de change de la devise vers l'euro (EUR)
1072 pour chaque année budgétaire.
1073 """
1074 # Identification
1075 devise = models.ForeignKey('Devise', db_column='devise')
1076 annee = models.IntegerField(verbose_name = u"Année")
1077 taux = models.FloatField(verbose_name = u"Taux vers l'euro")
1078
1079 class Meta:
1080 abstract = True
1081 ordering = ['-annee', 'devise__code']
1082 verbose_name = u"Taux de change"
1083 verbose_name_plural = u"Taux de change"
1084
1085 def __unicode__(self):
1086 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1087
1088
1089 class TauxChange(TauxChange_):
1090 __doc__ = TauxChange_.__doc__
1091
1092 class ValeurPointManager(NoDeleteManager):
1093 def get_query_set(self):
1094 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1095
1096
1097 class ValeurPoint_(AUFMetadata):
1098 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1099 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1100 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1101
1102 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1103 """
1104
1105 actuelles = ValeurPointManager()
1106
1107 valeur = models.FloatField(null=True)
1108 devise = models.ForeignKey('Devise', db_column='devise', null=True,
1109 related_name='+', default=5)
1110 implantation = models.ForeignKey(ref.Implantation,
1111 db_column='implantation',
1112 related_name='%(app_label)s_valeur_point')
1113 # Méta
1114 annee = models.IntegerField()
1115
1116 class Meta:
1117 ordering = ['-annee', 'implantation__nom']
1118 abstract = True
1119 verbose_name = u"Valeur du point"
1120 verbose_name_plural = u"Valeurs du point"
1121
1122 def __unicode__(self):
1123 return u'%s %s (%s)' % (self.valeur, self.devise, self.annee)
1124
1125
1126 class ValeurPoint(ValeurPoint_):
1127 __doc__ = ValeurPoint_.__doc__
1128
1129
1130 class Devise(AUFMetadata):
1131 """Devise monétaire.
1132 """
1133 code = models.CharField(max_length=10, unique=True)
1134 nom = models.CharField(max_length=255)
1135
1136 class Meta:
1137 ordering = ['code']
1138 verbose_name = u"Devise"
1139 verbose_name_plural = u"Devises"
1140
1141 def __unicode__(self):
1142 return u'%s - %s' % (self.code, self.nom)
1143
1144 class TypeContrat(AUFMetadata):
1145 """Type de contrat.
1146 """
1147 nom = models.CharField(max_length=255)
1148 nom_long = models.CharField(max_length=255)
1149
1150 class Meta:
1151 ordering = ['nom']
1152 verbose_name = u"Type de contrat"
1153 verbose_name_plural = u"Types de contrat"
1154
1155 def __unicode__(self):
1156 return u'%s' % (self.nom)
1157
1158
1159 ### AUTRES
1160
1161 class ResponsableImplantation(AUFMetadata):
1162 """Le responsable d'une implantation.
1163 Anciennement géré sur le Dossier du responsable.
1164 """
1165 employe = models.ForeignKey('Employe', db_column='employe',
1166 related_name='+',
1167 null=True, blank=True)
1168 implantation = models.ForeignKey(ref.Implantation,
1169 db_column='implantation', related_name='+',
1170 unique=True)
1171
1172 def __unicode__(self):
1173 return u'%s : %s' % (self.implantation, self.employe)
1174
1175 class Meta:
1176 ordering = ['implantation__nom']
1177 verbose_name = "Responsable d'implantation"
1178 verbose_name_plural = "Responsables d'implantation"