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