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