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