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