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