devise on current year
[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 return search.filter_temporel(self.rh_dossiers)
432
433 def dossiers_futurs(self):
434 params = {KEY_STATUT: STATUT_FUTUR, }
435 search = RechercheTemporelle(params, self.__class__)
436 search.purge_params(params)
437 return search.filter_temporel(self.rh_dossiers)
438
439 def dossiers_encours(self):
440 params = {KEY_STATUT: STATUT_ACTIF, }
441 search = RechercheTemporelle(params, self.__class__)
442 search.purge_params(params)
443 return search.filter_temporel(self.rh_dossiers)
444
445 def postes_encours(self):
446 postes_encours = set()
447 for d in self.dossiers_encours():
448 postes_encours.add(d.poste)
449 return postes_encours
450
451 def poste_principal(self):
452 """
453 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
454 Idée derrière :
455 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
456 """
457 poste = Poste.objects.none()
458 try:
459 poste = self.dossiers_encours().order_by('date_debut')[0].poste
460 except:
461 pass
462 return poste
463
464 prefix_implantation = "rh_dossiers__poste__implantation__region"
465 def get_regions(self):
466 regions = []
467 for d in self.dossiers.all():
468 regions.append(d.poste.implantation.region)
469 return regions
470
471
472 class EmployePiece(models.Model):
473 """Documents relatifs à un employé.
474 Ex.: CV...
475 """
476 employe = models.ForeignKey('Employe', db_column='employe')
477 nom = models.CharField(verbose_name="Nom", max_length=255)
478 fichier = models.FileField(verbose_name="Fichier",
479 upload_to=employe_piece_dispatch,
480 storage=storage_prive)
481
482 class Meta:
483 ordering = ['nom']
484 verbose_name = u"Employé pièce"
485 verbose_name_plural = u"Employé pièces"
486
487 def __unicode__(self):
488 return u'%s' % (self.nom)
489
490 class EmployeCommentaire(Commentaire):
491 employe = models.ForeignKey('Employe', db_column='employe',
492 related_name='+')
493
494 class Meta:
495 verbose_name = u"Employé commentaire"
496 verbose_name_plural = u"Employé commentaires"
497
498
499 LIEN_PARENTE_CHOICES = (
500 ('Conjoint', 'Conjoint'),
501 ('Conjointe', 'Conjointe'),
502 ('Fille', 'Fille'),
503 ('Fils', 'Fils'),
504 )
505
506 class AyantDroit(AUFMetadata):
507 """Personne en relation avec un Employe.
508 """
509 # Identification
510 nom = models.CharField(max_length=255)
511 prenom = models.CharField(max_length=255,
512 verbose_name = u"Prénom",)
513 nom_affichage = models.CharField(max_length=255,
514 verbose_name = u"Nom d'affichage",
515 null=True, blank=True)
516 nationalite = models.ForeignKey(ref.Pays, to_field='code',
517 db_column='nationalite',
518 related_name='ayantdroits_nationalite',
519 verbose_name = u"Nationalité",
520 null=True, blank=True)
521 date_naissance = models.DateField(verbose_name = u"Date de naissance",
522 help_text=HELP_TEXT_DATE,
523 validators=[validate_date_passee],
524 null=True, blank=True)
525 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
526
527 # Relation
528 employe = models.ForeignKey('Employe', db_column='employe',
529 related_name='ayantdroits',
530 verbose_name = u"Employé")
531 lien_parente = models.CharField(max_length=10,
532 choices=LIEN_PARENTE_CHOICES,
533 verbose_name = u"Lien de parenté",
534 null=True, blank=True)
535
536 class Meta:
537 ordering = ['nom', ]
538 verbose_name = u"Ayant droit"
539 verbose_name_plural = u"Ayants droit"
540
541 def __unicode__(self):
542 return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
543
544 prefix_implantation = "employe__dossiers__poste__implantation__region"
545 def get_regions(self):
546 regions = []
547 for d in self.employe.dossiers.all():
548 regions.append(d.poste.implantation.region)
549 return regions
550
551
552 class AyantDroitCommentaire(Commentaire):
553 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
554 related_name='+')
555
556
557 ### DOSSIER
558
559 STATUT_RESIDENCE_CHOICES = (
560 ('local', 'Local'),
561 ('expat', 'Expatrié'),
562 )
563
564 COMPTE_COMPTA_CHOICES = (
565 ('coda', 'CODA'),
566 ('scs', 'SCS'),
567 ('aucun', 'Aucun'),
568 )
569
570 class Dossier_(AUFMetadata, DevisableMixin):
571 """Le Dossier regroupe les informations relatives à l'occupation
572 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
573 par un Employe.
574
575 Plusieurs Contrats peuvent être associés au Dossier.
576 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
577 lequel aucun Dossier n'existe est un poste vacant.
578 """
579
580 objects = DossierManager()
581
582 # TODO: OneToOne ??
583 statut = models.ForeignKey('Statut', related_name='+', null=True)
584 organisme_bstg = models.ForeignKey('OrganismeBstg',
585 db_column='organisme_bstg',
586 related_name='+',
587 verbose_name = u"Organisme",
588 help_text="Si détaché (DET) ou \
589 mis à disposition (MAD), \
590 préciser l'organisme.",
591 null=True, blank=True)
592
593 # Recrutement
594 remplacement = models.BooleanField(default=False)
595 remplacement_de = models.ForeignKey('self', related_name='+',
596 help_text=u"Taper le nom de l'employé",
597 null=True, blank=True)
598 statut_residence = models.CharField(max_length=10, default='local',
599 verbose_name = u"Statut", null=True,
600 choices=STATUT_RESIDENCE_CHOICES)
601
602 # Rémunération
603 classement = models.ForeignKey('Classement', db_column='classement',
604 related_name='+',
605 null=True, blank=True)
606 regime_travail = models.DecimalField(max_digits=12, null=True,
607 decimal_places=2,
608 default=REGIME_TRAVAIL_DEFAULT,
609 verbose_name = u"Régime de travail",
610 help_text="% du temps complet")
611 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
612 decimal_places=2, null=True,
613 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
614 verbose_name=u"Nb. heures par semaine",
615 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT)
616
617 # Occupation du Poste par cet Employe (anciennement "mandat")
618 date_debut = models.DateField(verbose_name = u"Date de début d'occupation \
619 de poste",)
620 date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \
621 de poste",
622 null=True, blank=True)
623
624 # Comptes
625 # TODO?
626
627 class Meta:
628 abstract = True
629 ordering = ['employe__nom', ]
630 verbose_name = u"Dossier"
631 verbose_name_plural = "Dossiers"
632
633 def salaire_theorique(self):
634 annee = date.today().year
635 coeff = self.classement.coefficient
636 implantation = self.poste.implantation
637 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
638
639 montant = coeff * point.valeur
640 devise = point.devise
641 return {'montant':montant, 'devise':devise}
642
643 def __unicode__(self):
644 poste = self.poste.nom
645 if self.employe.genre == 'F':
646 poste = self.poste.nom_feminin
647 return u'%s - %s' % (self.employe, poste)
648
649 prefix_implantation = "poste__implantation__region"
650 def get_regions(self):
651 return [self.poste.implantation.region]
652
653
654 def remunerations(self):
655 return self.rh_remunerations.all().order_by('date_debut')
656
657 def remunerations_en_cours(self):
658 return self.rh_remunerations.all().filter(date_fin__exact=None).order_by('date_debut')
659
660 def get_salaire(self):
661 try:
662 return [r for r in self.remunerations().order_by('-date_debut') if r.type_id == 1][0]
663 except:
664 return None
665
666
667 class Dossier(Dossier_):
668 __doc__ = Dossier_.__doc__
669 poste = models.ForeignKey('%s.Poste' % app_context(),
670 db_column='poste',
671 related_name='%(app_label)s_dossiers',
672 help_text=u"Taper le nom du poste ou du type de poste",
673 )
674 employe = models.ForeignKey('Employe', db_column='employe',
675 help_text=u"Taper le nom de l'employé",
676 related_name='%(app_label)s_dossiers',
677 verbose_name=u"Employé")
678 principal = models.BooleanField(verbose_name=u"Principal?", default=True,
679 help_text=u"Ce Dossier est pour le principal Poste occupé par l'Employé")
680
681
682 class DossierPiece_(models.Model):
683 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
684 Ex.: Lettre de motivation.
685 """
686 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_dossierpieces')
687 nom = models.CharField(verbose_name = u"Nom", max_length=255)
688 fichier = models.FileField(verbose_name = u"Fichier",
689 upload_to=dossier_piece_dispatch,
690 storage=storage_prive)
691
692 class Meta:
693 abstract = True
694 ordering = ['nom']
695
696 def __unicode__(self):
697 return u'%s' % (self.nom)
698
699 class DossierPiece(DossierPiece_):
700 pass
701
702 class DossierCommentaire_(Commentaire):
703 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+')
704 class Meta:
705 abstract = True
706
707 class DossierCommentaire(DossierCommentaire_):
708 pass
709
710 class DossierComparaison_(models.Model, DevisableMixin):
711 """
712 Photo d'une comparaison salariale au moment de l'embauche.
713 """
714 dossier = models.ForeignKey('%s.Dossier' % app_context(), related_name='%(app_label)s_comparaisons')
715 objects = DossierComparaisonManager()
716
717 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
718 poste = models.CharField(max_length=255, null=True, blank=True)
719 personne = models.CharField(max_length=255, null=True, blank=True)
720 montant = models.IntegerField(null=True)
721 devise = models.ForeignKey('Devise', related_name='+', null=True, blank=True)
722
723 class Meta:
724 abstract = True
725
726
727 class DossierComparaison(DossierComparaison_):
728 pass
729
730 ### RÉMUNÉRATION
731
732 class RemunerationMixin(AUFMetadata):
733 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_remunerations')
734 # Identification
735 type = models.ForeignKey('TypeRemuneration', db_column='type',
736 related_name='+',
737 verbose_name = u"Type de rémunération")
738 type_revalorisation = models.ForeignKey('TypeRevalorisation',
739 db_column='type_revalorisation',
740 related_name='+',
741 verbose_name = u"Type de revalorisation",
742 null=True, blank=True)
743 montant = models.DecimalField(null=True, blank=True,
744 default=0, max_digits=12, decimal_places=2)
745 # Annuel (12 mois, 52 semaines, 364 jours?)
746 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
747 # commentaire = precision
748 commentaire = models.CharField(max_length=255, null=True, blank=True)
749 # date_debut = anciennement date_effectif
750 date_debut = models.DateField(verbose_name = u"Date de début",
751 null=True, blank=True)
752 date_fin = models.DateField(verbose_name = u"Date de fin",
753 null=True, blank=True)
754
755 class Meta:
756 abstract = True
757 ordering = ['type__nom', '-date_fin']
758
759 def __unicode__(self):
760 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
761
762 class Remuneration_(RemunerationMixin, DevisableMixin):
763 """Structure de rémunération (données budgétaires) en situation normale
764 pour un Dossier. Si un Evenement existe, utiliser la structure de
765 rémunération EvenementRemuneration de cet événement.
766 """
767
768 def montant_mois(self):
769 return round(self.montant / 12, 2)
770
771 def montant_avec_regime(self):
772 return round(self.montant * (self.dossier.regime_travail/100), 2)
773
774 def montant_euro_mois(self):
775 return round(self.montant_euros() / 12, 2)
776
777 def __unicode__(self):
778 try:
779 devise = self.devise.code
780 except:
781 devise = "???"
782 return "%s %s" % (self.montant, devise)
783
784 class Meta:
785 abstract = True
786 verbose_name = u"Rémunération"
787 verbose_name_plural = u"Rémunérations"
788
789
790 class Remuneration(Remuneration_):
791 pass
792
793
794 ### CONTRATS
795
796 class ContratManager(NoDeleteManager):
797 def get_query_set(self):
798 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
799
800
801 class Contrat_(AUFMetadata):
802 """Document juridique qui encadre la relation de travail d'un Employe
803 pour un Poste particulier. Pour un Dossier (qui documente cette
804 relation de travail) plusieurs contrats peuvent être associés.
805 """
806 objects = ContratManager()
807 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_contrats')
808 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
809 related_name='+',
810 verbose_name = u"type de contrat")
811 date_debut = models.DateField(verbose_name = u"Date de début")
812 date_fin = models.DateField(verbose_name = u"Date de fin",
813 null=True, blank=True)
814 fichier = models.FileField(verbose_name = u"Fichier",
815 upload_to=contrat_dispatch,
816 storage=storage_prive,
817 null=True, blank=True)
818
819 class Meta:
820 abstract = True
821 ordering = ['dossier__employe__nom']
822 verbose_name = u"Contrat"
823 verbose_name_plural = u"Contrats"
824
825 def __unicode__(self):
826 return u'%s - %s' % (self.dossier, self.id)
827
828 class Contrat(Contrat_):
829 pass
830
831
832 ### ÉVÉNEMENTS
833
834 #class Evenement_(AUFMetadata):
835 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
836 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
837 # (ex.: la Remuneration).
838 #
839 # Ex.: congé de maternité, maladie...
840 #
841 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
842 # différent et une rémunération en conséquence. On souhaite toutefois
843 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
844 # du retour à la normale.
845 # """
846 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
847 # related_name='+')
848 # nom = models.CharField(max_length=255)
849 # date_debut = models.DateField(verbose_name = u"Date de début")
850 # date_fin = models.DateField(verbose_name = u"Date de fin",
851 # null=True, blank=True)
852 #
853 # class Meta:
854 # abstract = True
855 # ordering = ['nom']
856 # verbose_name = u"Évènement"
857 # verbose_name_plural = u"Évènements"
858 #
859 # def __unicode__(self):
860 # return u'%s' % (self.nom)
861 #
862 #
863 #class Evenement(Evenement_):
864 # __doc__ = Evenement_.__doc__
865 #
866 #
867 #class EvenementRemuneration_(RemunerationMixin):
868 # """Structure de rémunération liée à un Evenement qui remplace
869 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
870 # de l'Evenement.
871 # """
872 # evenement = models.ForeignKey("Evenement", db_column='evenement',
873 # related_name='+',
874 # verbose_name = u"Évènement")
875 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
876 # # de l'Evenement associé
877 #
878 # class Meta:
879 # abstract = True
880 # ordering = ['evenement', 'type__nom', '-date_fin']
881 # verbose_name = u"Évènement - rémunération"
882 # verbose_name_plural = u"Évènements - rémunérations"
883 #
884 #
885 #class EvenementRemuneration(EvenementRemuneration_):
886 # __doc__ = EvenementRemuneration_.__doc__
887 #
888 # class Meta:
889 # abstract = True
890 #
891 #
892 #class EvenementRemuneration(EvenementRemuneration_):
893 # __doc__ = EvenementRemuneration_.__doc__
894 # TODO? class ContratPiece(models.Model):
895
896
897 ### RÉFÉRENCES RH
898
899 class FamilleEmploi(AUFMetadata):
900 """Catégorie utilisée dans la gestion des Postes.
901 Catégorie supérieure à TypePoste.
902 """
903 nom = models.CharField(max_length=255)
904
905 class Meta:
906 ordering = ['nom']
907 verbose_name = u"Famille d'emploi"
908 verbose_name_plural = u"Familles d'emploi"
909
910 def __unicode__(self):
911 return u'%s' % (self.nom)
912
913 class TypePoste(AUFMetadata):
914 """Catégorie de Poste.
915 """
916 nom = models.CharField(max_length=255)
917 nom_feminin = models.CharField(max_length=255,
918 verbose_name = u"Nom féminin")
919
920 is_responsable = models.BooleanField(default=False,
921 verbose_name = u"Poste de responsabilité")
922 famille_emploi = models.ForeignKey('FamilleEmploi',
923 db_column='famille_emploi',
924 related_name='+',
925 verbose_name = u"famille d'emploi")
926
927 class Meta:
928 ordering = ['nom']
929 verbose_name = u"Type de poste"
930 verbose_name_plural = u"Types de poste"
931
932 def __unicode__(self):
933 return u'%s' % (self.nom)
934
935
936 TYPE_PAIEMENT_CHOICES = (
937 (u'Régulier', u'Régulier'),
938 (u'Ponctuel', u'Ponctuel'),
939 )
940
941 NATURE_REMUNERATION_CHOICES = (
942 (u'Accessoire', u'Accessoire'),
943 (u'Charges', u'Charges'),
944 (u'Indemnité', u'Indemnité'),
945 (u'RAS', u'Rémunération autre source'),
946 (u'Traitement', u'Traitement'),
947 )
948
949 class TypeRemuneration(AUFMetadata):
950 """Catégorie de Remuneration.
951 """
952 objects = TypeRemunerationManager()
953
954 nom = models.CharField(max_length=255)
955 type_paiement = models.CharField(max_length=30,
956 choices=TYPE_PAIEMENT_CHOICES,
957 verbose_name = u"Type de paiement")
958 nature_remuneration = models.CharField(max_length=30,
959 choices=NATURE_REMUNERATION_CHOICES,
960 verbose_name = u"Nature de la rémunération")
961 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
962
963 class Meta:
964 ordering = ['nom']
965 verbose_name = u"Type de rémunération"
966 verbose_name_plural = u"Types de rémunération"
967
968 def __unicode__(self):
969 if self.archive:
970 archive = u"(archivé)"
971 else:
972 archive = ""
973 return u'%s %s' % (self.nom, archive)
974
975 class TypeRevalorisation(AUFMetadata):
976 """Justification du changement de la Remuneration.
977 (Actuellement utilisé dans aucun traitement informatique.)
978 """
979 nom = models.CharField(max_length=255)
980
981 class Meta:
982 ordering = ['nom']
983 verbose_name = u"Type de revalorisation"
984 verbose_name_plural = u"Types de revalorisation"
985
986 def __unicode__(self):
987 return u'%s' % (self.nom)
988
989 class Service(AUFMetadata):
990 """Unité administrative où les Postes sont rattachés.
991 """
992 objects = ServiceManager()
993
994 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
995 nom = models.CharField(max_length=255)
996
997 class Meta:
998 ordering = ['nom']
999 verbose_name = u"Service"
1000 verbose_name_plural = u"Services"
1001
1002 def __unicode__(self):
1003 if self.archive:
1004 archive = u"(archivé)"
1005 else:
1006 archive = ""
1007 return u'%s %s' % (self.nom, archive)
1008
1009
1010 TYPE_ORGANISME_CHOICES = (
1011 ('MAD', 'Mise à disposition'),
1012 ('DET', 'Détachement'),
1013 )
1014
1015 class OrganismeBstg(AUFMetadata):
1016 """Organisation d'où provient un Employe mis à disposition (MAD) de
1017 ou détaché (DET) à l'AUF à titre gratuit.
1018
1019 (BSTG = bien et service à titre gratuit.)
1020 """
1021 nom = models.CharField(max_length=255)
1022 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
1023 pays = models.ForeignKey(ref.Pays, to_field='code',
1024 db_column='pays',
1025 related_name='organismes_bstg',
1026 null=True, blank=True)
1027
1028 class Meta:
1029 ordering = ['type', 'nom']
1030 verbose_name = u"Organisme BSTG"
1031 verbose_name_plural = u"Organismes BSTG"
1032
1033 def __unicode__(self):
1034 return u'%s (%s)' % (self.nom, self.get_type_display())
1035
1036 prefix_implantation = "pays__region"
1037 def get_regions(self):
1038 return [self.pays.region]
1039
1040
1041 class Statut(AUFMetadata):
1042 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1043 """
1044 # Identification
1045 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.")
1046 nom = models.CharField(max_length=255)
1047
1048 class Meta:
1049 ordering = ['code']
1050 verbose_name = u"Statut d'employé"
1051 verbose_name_plural = u"Statuts d'employé"
1052
1053 def __unicode__(self):
1054 return u'%s : %s' % (self.code, self.nom)
1055
1056
1057 TYPE_CLASSEMENT_CHOICES = (
1058 ('S', 'S -Soutien'),
1059 ('T', 'T - Technicien'),
1060 ('P', 'P - Professionel'),
1061 ('C', 'C - Cadre'),
1062 ('D', 'D - Direction'),
1063 ('SO', 'SO - Sans objet [expatriés]'),
1064 ('HG', 'HG - Hors grille [direction]'),
1065 )
1066
1067 class ClassementManager(models.Manager):
1068 """
1069 Ordonner les spcéfiquement les classements.
1070 """
1071 def get_query_set(self):
1072 qs = super(self.__class__, self).get_query_set()
1073 qs = qs.extra(select={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1074 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
1075 return qs.all()
1076
1077
1078 class Classement_(AUFMetadata):
1079 """Éléments de classement de la
1080 "Grille générique de classement hiérarchique".
1081
1082 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1083 classement dans la grille. Le classement donne le coefficient utilisé dans:
1084
1085 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1086 """
1087 objects = ClassementManager()
1088
1089 # Identification
1090 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1091 echelon = models.IntegerField(verbose_name=u"Échelon", blank=True, default=0)
1092 degre = models.IntegerField(verbose_name=u"Degré", blank=True, default=0)
1093 coefficient = models.FloatField(default=0, verbose_name=u"Coefficient",
1094 null=True)
1095 # Méta
1096 # annee # au lieu de date_debut et date_fin
1097 commentaire = models.TextField(null=True, blank=True)
1098
1099 class Meta:
1100 abstract = True
1101 ordering = ['type','echelon','degre','coefficient']
1102 verbose_name = u"Classement"
1103 verbose_name_plural = u"Classements"
1104
1105 def __unicode__(self):
1106 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
1107
1108 class Classement(Classement_):
1109 __doc__ = Classement_.__doc__
1110
1111
1112 class TauxChange_(AUFMetadata):
1113 """Taux de change de la devise vers l'euro (EUR)
1114 pour chaque année budgétaire.
1115 """
1116 # Identification
1117 devise = models.ForeignKey('Devise', db_column='devise')
1118 annee = models.IntegerField(verbose_name = u"Année")
1119 taux = models.FloatField(verbose_name = u"Taux vers l'euro")
1120
1121 class Meta:
1122 abstract = True
1123 ordering = ['-annee', 'devise__code']
1124 verbose_name = u"Taux de change"
1125 verbose_name_plural = u"Taux de change"
1126
1127 def __unicode__(self):
1128 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1129
1130
1131 class TauxChange(TauxChange_):
1132 __doc__ = TauxChange_.__doc__
1133
1134 class ValeurPointManager(NoDeleteManager):
1135
1136 def get_query_set(self):
1137 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1138
1139
1140 class ValeurPoint_(AUFMetadata):
1141 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1142 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1143 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1144
1145 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1146 """
1147
1148 actuelles = ValeurPointManager()
1149
1150 valeur = models.FloatField(null=True)
1151 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
1152 implantation = models.ForeignKey(ref.Implantation,
1153 db_column='implantation',
1154 related_name='%(app_label)s_valeur_point')
1155 # Méta
1156 annee = models.IntegerField()
1157
1158 class Meta:
1159 ordering = ['-annee', 'implantation__nom']
1160 abstract = True
1161 verbose_name = u"Valeur du point"
1162 verbose_name_plural = u"Valeurs du point"
1163
1164 def __unicode__(self):
1165 return u'%s %s %s [%s] %s' % (self.devise.code, self.annee, self.valeur, self.implantation.nom_court, self.devise.nom)
1166
1167
1168 class ValeurPoint(ValeurPoint_):
1169 __doc__ = ValeurPoint_.__doc__
1170
1171
1172
1173 class Devise(AUFMetadata):
1174 """Devise monétaire.
1175 """
1176
1177 objects = DeviseManager()
1178
1179 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1180 code = models.CharField(max_length=10, unique=True)
1181 nom = models.CharField(max_length=255)
1182
1183 class Meta:
1184 ordering = ['code']
1185 verbose_name = u"Devise"
1186 verbose_name_plural = u"Devises"
1187
1188 def __unicode__(self):
1189 return u'%s - %s' % (self.code, self.nom)
1190
1191 class TypeContrat(AUFMetadata):
1192 """Type de contrat.
1193 """
1194 nom = models.CharField(max_length=255)
1195 nom_long = models.CharField(max_length=255)
1196
1197 class Meta:
1198 ordering = ['nom']
1199 verbose_name = u"Type de contrat"
1200 verbose_name_plural = u"Types de contrat"
1201
1202 def __unicode__(self):
1203 return u'%s' % (self.nom)
1204
1205
1206 ### AUTRES
1207
1208 class ResponsableImplantation(AUFMetadata):
1209 """Le responsable d'une implantation.
1210 Anciennement géré sur le Dossier du responsable.
1211 """
1212 employe = models.ForeignKey('Employe', db_column='employe',
1213 related_name='+',
1214 null=True, blank=True)
1215 implantation = models.ForeignKey(ref.Implantation,
1216 db_column='implantation', related_name='+',
1217 unique=True)
1218
1219 def __unicode__(self):
1220 return u'%s : %s' % (self.implantation, self.employe)
1221
1222 class Meta:
1223 ordering = ['implantation__nom']
1224 verbose_name = "Responsable d'implantation"
1225 verbose_name_plural = "Responsables d'implantation"
1226