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