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