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