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