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