boostup
[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, help_text=u"Taper le nom de l'implantation ou sa région",
84 db_column='implantation', related_name='+')
85 type_poste = models.ForeignKey('TypePoste', db_column='type_poste', help_text=u"Taper le nom du type de 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,help_text=u"Taper le nom du poste ou du type de poste",
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', help_text=u"Taper le code ou le nom de l'implantation",
126 db_column='valeur_point_min', related_name='+',
127 null=True, blank=True)
128 valeur_point_max = models.ForeignKey('ValeurPoint', help_text=u"Taper le code ou le nom de l'implantation",
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_debut = models.DateField(verbose_name=u"Date de début",
179 null=True, blank=True)
180 date_fin = models.DateField(verbose_name=u"Date de fin",
181 null=True, blank=True)
182
183 class Meta:
184 abstract = True
185 ordering = ['implantation__nom', 'nom']
186 verbose_name = u"Poste"
187 verbose_name_plural = u"Postes"
188
189 def __unicode__(self):
190 representation = u'%s - %s [%s]' % (self.implantation, self.nom,
191 self.id)
192 #if self.is_vacant():
193 # representation = representation + u' (VACANT)'
194 return representation
195
196 def is_vacant(self):
197 vacant = True
198 if self.occupe_par():
199 vacant = False
200 return vacant
201
202 def occupe_par(self):
203 """Retourne la liste d'employé occupant ce poste.
204 Généralement, retourne une liste d'un élément.
205 Si poste inoccupé, retourne liste vide.
206 """
207 return [d.employe for d in self.rh_dossiers.filter(actif=True, supprime=False) \
208 .exclude(date_fin__lt=date.today())]
209
210 prefix_implantation = "implantation__region"
211 def get_regions(self):
212 return [self.implantation.region]
213
214
215 class Poste(Poste_):
216 __doc__ = Poste_.__doc__
217
218
219 POSTE_FINANCEMENT_CHOICES = (
220 ('A', 'A - Frais de personnel'),
221 ('B', 'B - Projet(s)-Titre(s)'),
222 ('C', 'C - Autre')
223 )
224
225
226 class PosteFinancement_(models.Model):
227 """Pour un Poste, structure d'informations décrivant comment on prévoit
228 financer ce Poste.
229 """
230 poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='%(app_label)s_financements')
231 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
232 pourcentage = models.DecimalField(max_digits=12, decimal_places=2,
233 help_text="ex.: 33.33 % (décimale avec point)")
234 commentaire = models.TextField(
235 help_text="Spécifiez la source de financement.")
236
237 class Meta:
238 abstract = True
239 ordering = ['type']
240
241 def __unicode__(self):
242 return u'%s : %s %%' % (self.type, self.pourcentage)
243
244
245 class PosteFinancement(PosteFinancement_):
246 pass
247
248
249 class PostePiece_(models.Model):
250 """Documents relatifs au Poste.
251 Ex.: Description de poste
252 """
253 poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='%(app_label)s_pieces')
254 nom = models.CharField(verbose_name = u"Nom", max_length=255)
255 fichier = models.FileField(verbose_name = u"Fichier",
256 upload_to=poste_piece_dispatch,
257 storage=storage_prive)
258
259 class Meta:
260 abstract = True
261 ordering = ['nom']
262
263 def __unicode__(self):
264 return u'%s' % (self.nom)
265
266 class PostePiece(PostePiece_):
267 pass
268
269 class PosteComparaison_(models.Model):
270 """
271 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
272 """
273 poste = models.ForeignKey('%s.Poste' % app_context(), related_name='%(app_label)s_comparaisons_internes')
274 objects = PosteComparaisonManager()
275
276 implantation = models.ForeignKey(ref.Implantation, null=True, blank=True, related_name="+")
277 nom = models.CharField(verbose_name = u"Poste", max_length=255, null=True, blank=True)
278 montant = models.IntegerField(null=True)
279 devise = models.ForeignKey("Devise", default=5, related_name='+', null=True, blank=True)
280
281 class Meta:
282 abstract = True
283
284 def taux_devise(self):
285 if self.devise.code == "EUR":
286 return 1
287 annee = self.poste.date_debut.year
288 taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise, annee=annee)]
289 taux = set(taux)
290 if len(taux) != 1:
291 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))
292 else:
293 return list(taux)[0]
294
295 def montant_euros(self):
296 return round(float(self.montant) * float(self.taux_devise()), 2)
297
298 class PosteComparaison(PosteComparaison_):
299 pass
300
301 class PosteCommentaire_(Commentaire):
302 poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='+')
303
304 class Meta:
305 abstract = True
306
307 class PosteCommentaire(PosteCommentaire_):
308 pass
309
310 ### EMPLOYÉ/PERSONNE
311
312 class Employe(AUFMetadata):
313 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
314 Dossiers qu'il occupe ou a occupé de Postes.
315
316 Cette classe aurait pu avantageusement s'appeler Personne car la notion
317 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
318 """
319 # Identification
320 nom = models.CharField(max_length=255)
321 prenom = models.CharField(max_length=255, verbose_name = u"Prénom")
322 nom_affichage = models.CharField(max_length=255,
323 verbose_name = u"Nom d'affichage",
324 null=True, blank=True)
325 nationalite = models.ForeignKey(ref.Pays, to_field='code',
326 db_column='nationalite',
327 related_name='employes_nationalite',
328 verbose_name = u"Nationalité",
329 blank=True, null=True)
330 date_naissance = models.DateField(verbose_name = u"Date de naissance",
331 help_text=HELP_TEXT_DATE,
332 validators=[validate_date_passee],
333 null=True, blank=True)
334 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
335
336 # Infos personnelles
337 situation_famille = models.CharField(max_length=1,
338 choices=SITUATION_CHOICES,
339 verbose_name = u"Situation familiale",
340 null=True, blank=True)
341 date_entree = models.DateField(verbose_name = u"Date d'entrée à l'AUF",
342 help_text=HELP_TEXT_DATE,
343 null=True, blank=True)
344
345 # Coordonnées
346 tel_domicile = models.CharField(max_length=255,
347 verbose_name = u"Tél. domicile",
348 null=True, blank=True)
349 tel_cellulaire = models.CharField(max_length=255,
350 verbose_name = u"Tél. cellulaire",
351 null=True, blank=True)
352 adresse = models.CharField(max_length=255, null=True, blank=True)
353 ville = models.CharField(max_length=255, null=True, blank=True)
354 province = models.CharField(max_length=255, null=True, blank=True)
355 code_postal = models.CharField(max_length=255, null=True, blank=True)
356 pays = models.ForeignKey(ref.Pays, to_field='code', db_column='pays',
357 related_name='employes',
358 null=True, blank=True)
359
360 class Meta:
361 ordering = ['nom','prenom']
362 verbose_name = u"Employé"
363 verbose_name_plural = u"Employés"
364
365 def __unicode__(self):
366 return u'%s [%s]' % (self.get_nom(), self.id)
367
368 def get_nom(self):
369 nom_affichage = self.nom_affichage
370 if not nom_affichage:
371 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
372 return nom_affichage
373
374 def civilite(self):
375 civilite = u''
376 if self.genre.upper() == u'M':
377 civilite = u'M.'
378 elif self.genre.upper() == u'F':
379 civilite = u'Mme'
380 return civilite
381
382 def url_photo(self):
383 """Retourne l'URL du service retournant la photo de l'Employe.
384 Équivalent reverse url 'rh_photo' avec id en param.
385 """
386 from django.core.urlresolvers import reverse
387 return reverse('rh_photo', kwargs={'id':self.id})
388
389 def dossiers_passes(self):
390 today = date.today()
391 dossiers_passes = self.dossiers.filter(date_fin__lt=today).order_by('-date_fin')
392 for d in dossiers_passes:
393 d.archive = True
394 return dossiers_passes
395
396 def dossiers_futurs(self):
397 today = date.today()
398 return self.dossiers.filter(date_debut__gt=today).order_by('-date_fin')
399
400 def dossiers_encours(self):
401 dossiers_p_f = self.dossiers_passes() | self.dossiers_futurs()
402 ids_dossiers_p_f = [d.id for d in dossiers_p_f]
403 dossiers_encours = self.dossiers.exclude(id__in=ids_dossiers_p_f).order_by('-date_fin')
404
405 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
406 for d in dossiers_encours:
407 d.remunerations = Remuneration.objects.filter(dossier=d.id).order_by('-id')
408 return dossiers_encours
409
410 def postes_encours(self):
411 postes_encours = set()
412 for d in self.dossiers_encours():
413 postes_encours.add(d.poste)
414 return postes_encours
415
416 def poste_principal(self):
417 """
418 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
419 Idée derrière :
420 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
421 """
422 poste = Poste.objects.none()
423 try:
424 poste = self.dossiers_encours().order_by('date_debut')[0].poste
425 except:
426 pass
427 return poste
428
429 prefix_implantation = "dossiers__poste__implantation__region"
430 def get_regions(self):
431 regions = []
432 for d in self.dossiers.all():
433 regions.append(d.poste.implantation.region)
434 return regions
435
436
437 class EmployePiece(models.Model):
438 """Documents relatifs à un employé.
439 Ex.: CV...
440 """
441 employe = models.ForeignKey('Employe', db_column='employe')
442 nom = models.CharField(verbose_name="Nom", max_length=255)
443 fichier = models.FileField(verbose_name="Fichier",
444 upload_to=employe_piece_dispatch,
445 storage=storage_prive)
446
447 class Meta:
448 ordering = ['nom']
449 verbose_name = u"Employé pièce"
450 verbose_name_plural = u"Employé pièces"
451
452 def __unicode__(self):
453 return u'%s' % (self.nom)
454
455 class EmployeCommentaire(Commentaire):
456 employe = models.ForeignKey('Employe', db_column='employe',
457 related_name='+')
458
459 class Meta:
460 verbose_name = u"Employé commentaire"
461 verbose_name_plural = u"Employé commentaires"
462
463
464 LIEN_PARENTE_CHOICES = (
465 ('Conjoint', 'Conjoint'),
466 ('Conjointe', 'Conjointe'),
467 ('Fille', 'Fille'),
468 ('Fils', 'Fils'),
469 )
470
471 class AyantDroit(AUFMetadata):
472 """Personne en relation avec un Employe.
473 """
474 # Identification
475 nom = models.CharField(max_length=255)
476 prenom = models.CharField(max_length=255,
477 verbose_name = u"Prénom",)
478 nom_affichage = models.CharField(max_length=255,
479 verbose_name = u"Nom d'affichage",
480 null=True, blank=True)
481 nationalite = models.ForeignKey(ref.Pays, to_field='code',
482 db_column='nationalite',
483 related_name='ayantdroits_nationalite',
484 verbose_name = u"Nationalité",
485 null=True, blank=True)
486 date_naissance = models.DateField(verbose_name = u"Date de naissance",
487 help_text=HELP_TEXT_DATE,
488 validators=[validate_date_passee],
489 null=True, blank=True)
490 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
491
492 # Relation
493 employe = models.ForeignKey('Employe', db_column='employe',
494 related_name='ayantdroits',
495 verbose_name = u"Employé")
496 lien_parente = models.CharField(max_length=10,
497 choices=LIEN_PARENTE_CHOICES,
498 verbose_name = u"Lien de parenté",
499 null=True, blank=True)
500
501 class Meta:
502 ordering = ['nom_affichage']
503 verbose_name = u"Ayant droit"
504 verbose_name_plural = u"Ayants droit"
505
506 def __unicode__(self):
507 return u'%s' % (self.get_nom())
508
509 def get_nom(self):
510 nom_affichage = self.nom_affichage
511 if not nom_affichage:
512 nom_affichage = u'%s %s' % (self.nom.upper(), self.prenom)
513 return nom_affichage
514
515 prefix_implantation = "employe__dossiers__poste__implantation__region"
516 def get_regions(self):
517 regions = []
518 for d in self.employe.dossiers.all():
519 regions.append(d.poste.implantation.region)
520 return regions
521
522
523 class AyantDroitCommentaire(Commentaire):
524 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
525 related_name='+')
526
527
528 ### DOSSIER
529
530 STATUT_RESIDENCE_CHOICES = (
531 ('local', 'Local'),
532 ('expat', 'Expatrié'),
533 )
534
535 COMPTE_COMPTA_CHOICES = (
536 ('coda', 'CODA'),
537 ('scs', 'SCS'),
538 ('aucun', 'Aucun'),
539 )
540
541 class Dossier_(AUFMetadata):
542 """Le Dossier regroupe les informations relatives à l'occupation
543 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
544 par un Employe.
545
546 Plusieurs Contrats peuvent être associés au Dossier.
547 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
548 lequel aucun Dossier n'existe est un poste vacant.
549 """
550
551 objects = DossierManager()
552
553 # TODO: OneToOne ??
554 statut = models.ForeignKey('Statut', related_name='+', default=3,
555 null=True)
556 organisme_bstg = models.ForeignKey('OrganismeBstg',
557 db_column='organisme_bstg',
558 related_name='+',
559 verbose_name = u"Organisme",
560 help_text="Si détaché (DET) ou \
561 mis à disposition (MAD), \
562 préciser l'organisme.",
563 null=True, blank=True)
564
565 # Recrutement
566 remplacement = models.BooleanField(default=False)
567 remplacement_de = models.ForeignKey('self', related_name='+',
568 help_text=u"Taper le nom de l'employé",
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(),
637 db_column='poste',
638 related_name='%(app_label)s_dossiers',
639 help_text=u"Taper le nom du poste ou du type de poste",
640 )
641 employe = models.ForeignKey('Employe', db_column='employe',
642 help_text=u"Taper le nom de l'employé",
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
1100 def get_query_set(self):
1101 now = datetime.datetime.now()
1102 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1103
1104
1105 class ValeurPoint_(AUFMetadata):
1106 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1107 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1108 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1109
1110 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1111 """
1112
1113 actuelles = ValeurPointManager()
1114
1115 valeur = models.FloatField(null=True)
1116 devise = models.ForeignKey('Devise', db_column='devise', null=True,
1117 related_name='+', default=5)
1118 implantation = models.ForeignKey(ref.Implantation,
1119 db_column='implantation',
1120 related_name='%(app_label)s_valeur_point')
1121 # Méta
1122 annee = models.IntegerField()
1123
1124 class Meta:
1125 ordering = ['-annee', 'implantation__nom']
1126 abstract = True
1127 verbose_name = u"Valeur du point"
1128 verbose_name_plural = u"Valeurs du point"
1129
1130 def __unicode__(self):
1131 return u'%s %s [%s]' % (self.devise, self.annee, self.implantation.nom_court)
1132
1133
1134 class ValeurPoint(ValeurPoint_):
1135 __doc__ = ValeurPoint_.__doc__
1136
1137
1138
1139 class DeviseManager(NoDeleteManager):
1140
1141 def get_query_set(self):
1142 # exclure US et CAN
1143 return super(DeviseManager, self).get_query_set().exclude(id__in=(3, 15))
1144
1145 class Devise(AUFMetadata):
1146 """Devise monétaire.
1147 """
1148 objects = DeviseManager()
1149
1150 code = models.CharField(max_length=10, unique=True)
1151 nom = models.CharField(max_length=255)
1152
1153 class Meta:
1154 ordering = ['code']
1155 verbose_name = u"Devise"
1156 verbose_name_plural = u"Devises"
1157
1158 def __unicode__(self):
1159 return u'%s - %s' % (self.code, self.nom)
1160
1161 class TypeContrat(AUFMetadata):
1162 """Type de contrat.
1163 """
1164 nom = models.CharField(max_length=255)
1165 nom_long = models.CharField(max_length=255)
1166
1167 class Meta:
1168 ordering = ['nom']
1169 verbose_name = u"Type de contrat"
1170 verbose_name_plural = u"Types de contrat"
1171
1172 def __unicode__(self):
1173 return u'%s' % (self.nom)
1174
1175
1176 ### AUTRES
1177
1178 class ResponsableImplantation(AUFMetadata):
1179 """Le responsable d'une implantation.
1180 Anciennement géré sur le Dossier du responsable.
1181 """
1182 employe = models.ForeignKey('Employe', db_column='employe',
1183 related_name='+',
1184 null=True, blank=True)
1185 implantation = models.ForeignKey(ref.Implantation,
1186 db_column='implantation', related_name='+',
1187 unique=True)
1188
1189 def __unicode__(self):
1190 return u'%s : %s' % (self.implantation, self.employe)
1191
1192 class Meta:
1193 ordering = ['implantation__nom']
1194 verbose_name = "Responsable d'implantation"
1195 verbose_name_plural = "Responsables d'implantation"