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