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