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