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