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