Fixed unicode des postes
[auf_rh_dae.git] / project / rh / models.py
1 # -=- encoding: utf-8 -=-
2
3 import datetime
4 from datetime import date
5 from decimal import Decimal
6
7 from django.db.models import signals
8 from django.core.files.storage import FileSystemStorage
9 from django.db import models
10 from django.conf import settings
11 from django.db.models import Q
12
13 from auf.django.emploi.models import GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references
14 from auf.django.metadata.models import AUFMetadata
15 from auf.django.metadata.managers import NoDeleteManager
16 import auf.django.references.models as ref
17 from validators import validate_date_passee
18 from managers import PosteManager, DossierManager, DossierComparaisonManager, PosteComparaisonManager, DeviseManager
19
20
21 # Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
22 # Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
23 def app_context():
24 import inspect;
25 models_stack = [s[1].split('/')[-2] for s in inspect.stack() if s[1].endswith('models.py')]
26 return models_stack[-1]
27
28
29 # Constantes
30 HELP_TEXT_DATE = "format: jj-mm-aaaa"
31 REGIME_TRAVAIL_DEFAULT = Decimal('100.00')
32 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = Decimal('35.00')
33
34 # Upload de fichiers
35 storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
36 base_url=settings.PRIVE_MEDIA_URL)
37
38 def poste_piece_dispatch(instance, filename):
39 path = "%s/poste/%s/%s" % (instance._meta.app_label, instance.poste_id, filename)
40 return path
41
42 def dossier_piece_dispatch(instance, filename):
43 path = "%s/dossier/%s/%s" % (instance._meta.app_label, instance.dossier_id, filename)
44 return path
45
46 def employe_piece_dispatch(instance, filename):
47 path = "%s/employe/%s/%s" % (instance._meta.app_label, instance.employe_id, filename)
48 return path
49
50 def contrat_dispatch(instance, filename):
51 path = "%s/contrat/%s/%s" % (instance._meta.app_label, instance.dossier_id, filename)
52 return path
53
54
55 class Commentaire(AUFMetadata):
56 texte = models.TextField()
57 owner = models.ForeignKey('auth.User', db_column='owner', related_name='+', verbose_name=u"Commentaire de")
58
59 class Meta:
60 abstract = True
61 ordering = ['-date_creation']
62
63 def __unicode__(self):
64 return u'%s' % (self.texte)
65
66
67 ### POSTE
68
69 POSTE_APPEL_CHOICES = (
70 ('interne', 'Interne'),
71 ('externe', 'Externe'),
72 )
73
74 class Poste_(AUFMetadata):
75 """Un Poste est un emploi (job) à combler dans une implantation.
76 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
77 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
78 """
79
80 objects = PosteManager()
81
82 # Identification
83 nom = models.CharField(max_length=255,
84 verbose_name = u"Titre du poste", )
85 nom_feminin = models.CharField(max_length=255,
86 verbose_name = u"Titre du poste (au féminin)",
87 null=True)
88 implantation = models.ForeignKey(ref.Implantation, help_text=u"Taper le nom de l'implantation ou sa région",
89 db_column='implantation', related_name='+')
90 type_poste = models.ForeignKey('TypePoste', db_column='type_poste', help_text=u"Taper le nom du type de poste",
91 related_name='+',
92 null=True,
93 verbose_name=u"type de poste")
94 service = models.ForeignKey('Service', db_column='service',
95 related_name='+',
96 verbose_name = u"direction/service/pôle support",
97 null=True,)
98 responsable = models.ForeignKey('Poste', db_column='responsable',
99 related_name='+',
100 null=True,
101 help_text=u"Taper le nom du poste ou du type de poste",
102 verbose_name = u"Poste du responsable", )
103
104 # Contrat
105 regime_travail = models.DecimalField(max_digits=12, decimal_places=2,
106 default=REGIME_TRAVAIL_DEFAULT, null=True,
107 verbose_name = u"Temps de travail",
108 help_text="% du temps complet")
109 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
110 decimal_places=2, null=True,
111 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
112 verbose_name = u"Nb. heures par semaine")
113
114 # Recrutement
115 local = models.NullBooleanField(verbose_name = u"Local", default=True,
116 null=True, blank=True)
117 expatrie = models.NullBooleanField(verbose_name = u"Expatrié", default=False,
118 null=True, blank=True)
119 mise_a_disposition = models.NullBooleanField(
120 verbose_name = u"Mise à disposition",
121 null=True, default=False)
122 appel = models.CharField(max_length=10, null=True,
123 verbose_name = u"Appel à candidature",
124 choices=POSTE_APPEL_CHOICES,
125 default='interne')
126
127 # Rémunération
128 classement_min = models.ForeignKey('Classement',
129 db_column='classement_min', related_name='+',
130 null=True, blank=True)
131 classement_max = models.ForeignKey('Classement',
132 db_column='classement_max', related_name='+',
133 null=True, blank=True)
134 valeur_point_min = models.ForeignKey('ValeurPoint', help_text=u"Taper le code ou le nom de l'implantation",
135 db_column='valeur_point_min', related_name='+',
136 null=True, blank=True)
137 valeur_point_max = models.ForeignKey('ValeurPoint', help_text=u"Taper le code ou le nom de l'implantation",
138 db_column='valeur_point_max', related_name='+',
139 null=True, blank=True)
140 devise_min = models.ForeignKey('Devise', db_column='devise_min', null=True,
141 related_name='+',)
142 devise_max = models.ForeignKey('Devise', db_column='devise_max', null=True,
143 related_name='+',)
144 salaire_min = models.DecimalField(max_digits=12, decimal_places=2,
145 null=True, default=0)
146 salaire_max = models.DecimalField(max_digits=12, decimal_places=2,
147 null=True, default=0)
148 indemn_min = models.DecimalField(max_digits=12, decimal_places=2,
149 null=True, default=0)
150 indemn_max = models.DecimalField(max_digits=12, decimal_places=2,
151 null=True, default=0)
152 autre_min = models.DecimalField(max_digits=12, decimal_places=2,
153 null=True, default=0)
154 autre_max = models.DecimalField(max_digits=12, decimal_places=2,
155 null=True, default=0)
156
157 # Comparatifs de rémunération
158 devise_comparaison = models.ForeignKey('Devise', null=True, blank=True,
159 db_column='devise_comparaison',
160 related_name='+', )
161 comp_locale_min = models.DecimalField(max_digits=12, decimal_places=2,
162 null=True, blank=True)
163 comp_locale_max = models.DecimalField(max_digits=12, decimal_places=2,
164 null=True, blank=True)
165 comp_universite_min = models.DecimalField(max_digits=12, decimal_places=2,
166 null=True, blank=True)
167 comp_universite_max = models.DecimalField(max_digits=12, decimal_places=2,
168 null=True, blank=True)
169 comp_fonctionpub_min = models.DecimalField(max_digits=12, decimal_places=2,
170 null=True, blank=True)
171 comp_fonctionpub_max = models.DecimalField(max_digits=12, decimal_places=2,
172 null=True, blank=True)
173 comp_ong_min = models.DecimalField(max_digits=12, decimal_places=2,
174 null=True, blank=True)
175 comp_ong_max = models.DecimalField(max_digits=12, decimal_places=2,
176 null=True, blank=True)
177 comp_autre_min = models.DecimalField(max_digits=12, decimal_places=2,
178 null=True, blank=True)
179 comp_autre_max = models.DecimalField(max_digits=12, decimal_places=2,
180 null=True, blank=True)
181
182 # Justification
183 justification = models.TextField(null=True, blank=True)
184
185 # Autres Metadata
186 date_debut = models.DateField(verbose_name=u"Date de début", help_text=HELP_TEXT_DATE,
187 null=True, blank=True)
188 date_fin = models.DateField(verbose_name=u"Date de fin", help_text=HELP_TEXT_DATE,
189 null=True, blank=True)
190
191 class Meta:
192 abstract = True
193 ordering = ['implantation__nom', 'nom']
194 verbose_name = u"Poste"
195 verbose_name_plural = u"Postes"
196 ordering = ["nom"]
197
198 def __unicode__(self):
199 representation = u'%s - %s [%s]' % (self.implantation, self.nom,
200 self.id)
201 return representation
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 class EmployeProxy(Employe):
477
478 class Meta:
479 proxy = True
480 verbose_name = u"Organigramme des employés"
481 verbose_name_plural = u"Organigramme des employés"
482
483 LIEN_PARENTE_CHOICES = (
484 ('Conjoint', 'Conjoint'),
485 ('Conjointe', 'Conjointe'),
486 ('Fille', 'Fille'),
487 ('Fils', 'Fils'),
488 )
489
490 class AyantDroit(AUFMetadata):
491 """Personne en relation avec un Employe.
492 """
493 # Identification
494 nom = models.CharField(max_length=255)
495 prenom = models.CharField(max_length=255,
496 verbose_name = u"Prénom",)
497 nom_affichage = models.CharField(max_length=255,
498 verbose_name = u"Nom d'affichage",
499 null=True, blank=True)
500 nationalite = models.ForeignKey(ref.Pays, to_field='code',
501 db_column='nationalite',
502 related_name='ayantdroits_nationalite',
503 verbose_name = u"Nationalité",
504 null=True, blank=True)
505 date_naissance = models.DateField(verbose_name = u"Date de naissance",
506 help_text=HELP_TEXT_DATE,
507 validators=[validate_date_passee],
508 null=True, blank=True)
509 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
510
511 # Relation
512 employe = models.ForeignKey('Employe', db_column='employe',
513 related_name='ayantdroits',
514 verbose_name = u"Employé")
515 lien_parente = models.CharField(max_length=10,
516 choices=LIEN_PARENTE_CHOICES,
517 verbose_name = u"Lien de parenté",
518 null=True, blank=True)
519
520 class Meta:
521 ordering = ['nom', ]
522 verbose_name = u"Ayant droit"
523 verbose_name_plural = u"Ayants droit"
524
525 def __unicode__(self):
526 return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
527
528 prefix_implantation = "employe__dossiers__poste__implantation__region"
529 def get_regions(self):
530 regions = []
531 for d in self.employe.dossiers.all():
532 regions.append(d.poste.implantation.region)
533 return regions
534
535
536 class AyantDroitCommentaire(Commentaire):
537 ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
538 related_name='+')
539
540
541 ### DOSSIER
542
543 STATUT_RESIDENCE_CHOICES = (
544 ('local', 'Local'),
545 ('expat', 'Expatrié'),
546 )
547
548 COMPTE_COMPTA_CHOICES = (
549 ('coda', 'CODA'),
550 ('scs', 'SCS'),
551 ('aucun', 'Aucun'),
552 )
553
554 class Dossier_(AUFMetadata):
555 """Le Dossier regroupe les informations relatives à l'occupation
556 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
557 par un Employe.
558
559 Plusieurs Contrats peuvent être associés au Dossier.
560 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
561 lequel aucun Dossier n'existe est un poste vacant.
562 """
563
564 objects = DossierManager()
565
566 # TODO: OneToOne ??
567 statut = models.ForeignKey('Statut', related_name='+', null=True)
568 organisme_bstg = models.ForeignKey('OrganismeBstg',
569 db_column='organisme_bstg',
570 related_name='+',
571 verbose_name = u"Organisme",
572 help_text="Si détaché (DET) ou \
573 mis à disposition (MAD), \
574 préciser l'organisme.",
575 null=True, blank=True)
576
577 # Recrutement
578 remplacement = models.BooleanField(default=False)
579 remplacement_de = models.ForeignKey('self', related_name='+',
580 help_text=u"Taper le nom de l'employé",
581 null=True, blank=True)
582 statut_residence = models.CharField(max_length=10, default='local',
583 verbose_name = u"Statut", null=True,
584 choices=STATUT_RESIDENCE_CHOICES)
585
586 # Rémunération
587 classement = models.ForeignKey('Classement', db_column='classement',
588 related_name='+',
589 null=True, blank=True)
590 regime_travail = models.DecimalField(max_digits=12, null=True,
591 decimal_places=2,
592 default=REGIME_TRAVAIL_DEFAULT,
593 verbose_name = u"Régime de travail",
594 help_text="% du temps complet")
595 regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
596 decimal_places=2, null=True,
597 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
598 verbose_name = u"Nb. heures par semaine")
599
600 # Occupation du Poste par cet Employe (anciennement "mandat")
601 date_debut = models.DateField(verbose_name = u"Date de début d'occupation \
602 de poste",)
603 date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \
604 de poste",
605 null=True, blank=True)
606
607 # Comptes
608 # TODO?
609
610 class Meta:
611 abstract = True
612 ordering = ['employe__nom', ]
613 verbose_name = u"Dossier"
614 verbose_name_plural = "Dossiers"
615
616 def salaire_theorique(self):
617 annee = date.today().year
618 coeff = self.classement.coefficient
619 implantation = self.poste.implantation
620 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
621
622 montant = coeff * point.valeur
623 devise = point.devise
624 return {'montant':montant, 'devise':devise}
625
626 def __unicode__(self):
627 poste = self.poste.nom
628 if self.employe.genre == 'F':
629 poste = self.poste.nom_feminin
630 return u'%s - %s' % (self.employe, poste)
631
632 prefix_implantation = "poste__implantation__region"
633 def get_regions(self):
634 return [self.poste.implantation.region]
635
636
637 def remunerations(self):
638 return self.rh_remunerations.all().order_by('date_debut')
639
640 def remunerations_en_cours(self):
641 return self.rh_remunerations.all().filter(date_fin__exact=None).order_by('date_debut')
642
643 def get_salaire(self):
644 try:
645 return [r for r in self.remunerations().order_by('-date_debut') if r.type_id == 1][0]
646 except:
647 return None
648
649 class Dossier(Dossier_):
650 __doc__ = Dossier_.__doc__
651 poste = models.ForeignKey('%s.Poste' % app_context(),
652 db_column='poste',
653 related_name='%(app_label)s_dossiers',
654 help_text=u"Taper le nom du poste ou du type de poste",
655 )
656 employe = models.ForeignKey('Employe', db_column='employe',
657 help_text=u"Taper le nom de l'employé",
658 related_name='%(app_label)s_dossiers',
659 verbose_name=u"Employé")
660
661
662 class DossierPiece_(models.Model):
663 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
664 Ex.: Lettre de motivation.
665 """
666 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+')
667 nom = models.CharField(verbose_name = u"Nom", max_length=255)
668 fichier = models.FileField(verbose_name = u"Fichier",
669 upload_to=dossier_piece_dispatch,
670 storage=storage_prive)
671
672 class Meta:
673 abstract = True
674 ordering = ['nom']
675
676 def __unicode__(self):
677 return u'%s' % (self.nom)
678
679 class DossierPiece(DossierPiece_):
680 pass
681
682 class DossierCommentaire_(Commentaire):
683 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+')
684 class Meta:
685 abstract = True
686
687 class DossierCommentaire(DossierCommentaire_):
688 pass
689
690 class DossierComparaison_(models.Model):
691 """
692 Photo d'une comparaison salariale au moment de l'embauche.
693 """
694 dossier = models.ForeignKey('%s.Dossier' % app_context(), related_name='%(app_label)s_comparaisons')
695 objects = DossierComparaisonManager()
696
697 implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
698 poste = models.CharField(max_length=255, null=True, blank=True)
699 personne = models.CharField(max_length=255, null=True, blank=True)
700 montant = models.IntegerField(null=True)
701 devise = models.ForeignKey('Devise', related_name='+', null=True, blank=True)
702
703 class Meta:
704 abstract = True
705
706 def taux_devise(self):
707 annee = self.dossier.poste.date_debut.year
708 taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise, annee=annee)]
709 taux = set(taux)
710 if len(taux) != 1:
711 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))
712 else:
713 return list(taux)[0]
714
715 def montant_euros(self):
716 return round(float(self.montant) * float(self.taux_devise()), 2)
717
718 class DossierComparaison(DossierComparaison_):
719 pass
720
721 ### RÉMUNÉRATION
722
723 class RemunerationMixin(AUFMetadata):
724 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_remunerations')
725 # Identification
726 type = models.ForeignKey('TypeRemuneration', db_column='type',
727 related_name='+',
728 verbose_name = u"Type de rémunération")
729 type_revalorisation = models.ForeignKey('TypeRevalorisation',
730 db_column='type_revalorisation',
731 related_name='+',
732 verbose_name = u"Type de revalorisation",
733 null=True, blank=True)
734 montant = models.DecimalField(null=True, blank=True,
735 default=0, max_digits=12, decimal_places=2)
736 # Annuel (12 mois, 52 semaines, 364 jours?)
737 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
738 # commentaire = precision
739 commentaire = models.CharField(max_length=255, null=True, blank=True)
740 # date_debut = anciennement date_effectif
741 date_debut = models.DateField(verbose_name = u"Date de début",
742 null=True, blank=True)
743 date_fin = models.DateField(verbose_name = u"Date de fin",
744 null=True, blank=True)
745
746 class Meta:
747 abstract = True
748 ordering = ['type__nom', '-date_fin']
749
750 def __unicode__(self):
751 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
752
753 class Remuneration_(RemunerationMixin):
754 """Structure de rémunération (données budgétaires) en situation normale
755 pour un Dossier. Si un Evenement existe, utiliser la structure de
756 rémunération EvenementRemuneration de cet événement.
757 """
758
759 def montant_mois(self):
760 return round(self.montant / 12, 2)
761
762 def taux_devise(self):
763 if self.devise.code == "EUR":
764 return 1
765
766 annee = datetime.datetime.now().year
767 if self.date_debut is not None:
768 annee = self.date_debut.year
769 if self.dossier.poste.date_debut is not None:
770 annee = self.dossier.poste.date_debut.year
771
772 taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise_id, annee=annee)]
773 taux = set(taux)
774 if len(taux) != 1:
775 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))
776 else:
777 return list(taux)[0]
778
779 def montant_euro(self):
780 return round(float(self.montant) * float(self.taux_devise()), 2)
781
782 def montant_euro_mois(self):
783 return round(self.montant_euro() / 12, 2)
784
785 def __unicode__(self):
786 try:
787 devise = self.devise.code
788 except:
789 devise = "???"
790 return "%s %s" % (self.montant, devise)
791
792 class Meta:
793 abstract = True
794 verbose_name = u"Rémunération"
795 verbose_name_plural = u"Rémunérations"
796
797
798 class Remuneration(Remuneration_):
799 pass
800
801
802 ### CONTRATS
803
804 class ContratManager(NoDeleteManager):
805 def get_query_set(self):
806 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
807
808
809 class Contrat_(AUFMetadata):
810 """Document juridique qui encadre la relation de travail d'un Employe
811 pour un Poste particulier. Pour un Dossier (qui documente cette
812 relation de travail) plusieurs contrats peuvent être associés.
813 """
814 objects = ContratManager()
815 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_contrats')
816 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
817 related_name='+',
818 verbose_name = u"type de contrat")
819 date_debut = models.DateField(verbose_name = u"Date de début")
820 date_fin = models.DateField(verbose_name = u"Date de fin",
821 null=True, blank=True)
822 fichier = models.FileField(verbose_name = u"Fichier",
823 upload_to=contrat_dispatch,
824 storage=storage_prive,
825 null=True, blank=True)
826
827 class Meta:
828 abstract = True
829 ordering = ['dossier__employe__nom']
830 verbose_name = u"Contrat"
831 verbose_name_plural = u"Contrats"
832
833 def __unicode__(self):
834 return u'%s - %s' % (self.dossier, self.id)
835
836 class Contrat(Contrat_):
837 pass
838
839
840 ### ÉVÉNEMENTS
841
842 #class Evenement_(AUFMetadata):
843 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
844 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
845 # (ex.: la Remuneration).
846 #
847 # Ex.: congé de maternité, maladie...
848 #
849 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
850 # différent et une rémunération en conséquence. On souhaite toutefois
851 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
852 # du retour à la normale.
853 # """
854 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
855 # related_name='+')
856 # nom = models.CharField(max_length=255)
857 # date_debut = models.DateField(verbose_name = u"Date de début")
858 # date_fin = models.DateField(verbose_name = u"Date de fin",
859 # null=True, blank=True)
860 #
861 # class Meta:
862 # abstract = True
863 # ordering = ['nom']
864 # verbose_name = u"Évènement"
865 # verbose_name_plural = u"Évènements"
866 #
867 # def __unicode__(self):
868 # return u'%s' % (self.nom)
869 #
870 #
871 #class Evenement(Evenement_):
872 # __doc__ = Evenement_.__doc__
873 #
874 #
875 #class EvenementRemuneration_(RemunerationMixin):
876 # """Structure de rémunération liée à un Evenement qui remplace
877 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
878 # de l'Evenement.
879 # """
880 # evenement = models.ForeignKey("Evenement", db_column='evenement',
881 # related_name='+',
882 # verbose_name = u"Évènement")
883 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
884 # # de l'Evenement associé
885 #
886 # class Meta:
887 # abstract = True
888 # ordering = ['evenement', 'type__nom', '-date_fin']
889 # verbose_name = u"Évènement - rémunération"
890 # verbose_name_plural = u"Évènements - rémunérations"
891 #
892 #
893 #class EvenementRemuneration(EvenementRemuneration_):
894 # __doc__ = EvenementRemuneration_.__doc__
895 #
896 # class Meta:
897 # abstract = True
898 #
899 #
900 #class EvenementRemuneration(EvenementRemuneration_):
901 # __doc__ = EvenementRemuneration_.__doc__
902 # TODO? class ContratPiece(models.Model):
903
904
905 ### RÉFÉRENCES RH
906
907 class FamilleEmploi(AUFMetadata):
908 """Catégorie utilisée dans la gestion des Postes.
909 Catégorie supérieure à TypePoste.
910 """
911 nom = models.CharField(max_length=255)
912
913 class Meta:
914 ordering = ['nom']
915 verbose_name = u"Famille d'emploi"
916 verbose_name_plural = u"Familles d'emploi"
917
918 def __unicode__(self):
919 return u'%s' % (self.nom)
920
921 class TypePoste(AUFMetadata):
922 """Catégorie de Poste.
923 """
924 nom = models.CharField(max_length=255)
925 nom_feminin = models.CharField(max_length=255,
926 verbose_name = u"Nom féminin")
927
928 is_responsable = models.BooleanField(default=False,
929 verbose_name = u"Poste de responsabilité")
930 famille_emploi = models.ForeignKey('FamilleEmploi',
931 db_column='famille_emploi',
932 related_name='+',
933 verbose_name = u"famille d'emploi")
934
935 class Meta:
936 ordering = ['nom']
937 verbose_name = u"Type de poste"
938 verbose_name_plural = u"Types de poste"
939
940 def __unicode__(self):
941 return u'%s' % (self.nom)
942
943
944 TYPE_PAIEMENT_CHOICES = (
945 (u'Régulier', u'Régulier'),
946 (u'Ponctuel', u'Ponctuel'),
947 )
948
949 NATURE_REMUNERATION_CHOICES = (
950 (u'Accessoire', u'Accessoire'),
951 (u'Charges', u'Charges'),
952 (u'Indemnité', u'Indemnité'),
953 (u'RAS', u'Rémunération autre source'),
954 (u'Traitement', u'Traitement'),
955 )
956
957 class TypeRemuneration(AUFMetadata):
958 """Catégorie de Remuneration.
959 """
960 nom = models.CharField(max_length=255)
961 type_paiement = models.CharField(max_length=30,
962 choices=TYPE_PAIEMENT_CHOICES,
963 verbose_name = u"Type de paiement")
964 nature_remuneration = models.CharField(max_length=30,
965 choices=NATURE_REMUNERATION_CHOICES,
966 verbose_name = u"Nature de la rémunération")
967
968 class Meta:
969 ordering = ['nom']
970 verbose_name = u"Type de rémunération"
971 verbose_name_plural = u"Types de rémunération"
972
973 def __unicode__(self):
974 return u'%s' % (self.nom)
975
976 class TypeRevalorisation(AUFMetadata):
977 """Justification du changement de la Remuneration.
978 (Actuellement utilisé dans aucun traitement informatique.)
979 """
980 nom = models.CharField(max_length=255)
981
982 class Meta:
983 ordering = ['nom']
984 verbose_name = u"Type de revalorisation"
985 verbose_name_plural = u"Types de revalorisation"
986
987 def __unicode__(self):
988 return u'%s' % (self.nom)
989
990 class Service(AUFMetadata):
991 """Unité administrative où les Postes sont rattachés.
992 """
993 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
994 nom = models.CharField(max_length=255)
995
996 class Meta:
997 ordering = ['nom']
998 verbose_name = u"Service"
999 verbose_name_plural = u"Services"
1000
1001 def __unicode__(self):
1002 if self.archive:
1003 archive = u"(archivé)"
1004 else:
1005 archive = ""
1006 return u'%s %s' % (self.nom, archive)
1007
1008
1009 TYPE_ORGANISME_CHOICES = (
1010 ('MAD', 'Mise à disposition'),
1011 ('DET', 'Détachement'),
1012 )
1013
1014 class OrganismeBstg(AUFMetadata):
1015 """Organisation d'où provient un Employe mis à disposition (MAD) de
1016 ou détaché (DET) à l'AUF à titre gratuit.
1017
1018 (BSTG = bien et service à titre gratuit.)
1019 """
1020 nom = models.CharField(max_length=255)
1021 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
1022 pays = models.ForeignKey(ref.Pays, to_field='code',
1023 db_column='pays',
1024 related_name='organismes_bstg',
1025 null=True, blank=True)
1026
1027 class Meta:
1028 ordering = ['type', 'nom']
1029 verbose_name = u"Organisme BSTG"
1030 verbose_name_plural = u"Organismes BSTG"
1031
1032 def __unicode__(self):
1033 return u'%s (%s)' % (self.nom, self.get_type_display())
1034
1035 prefix_implantation = "pays__region"
1036 def get_regions(self):
1037 return [self.pays.region]
1038
1039
1040 class Statut(AUFMetadata):
1041 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1042 """
1043 # Identification
1044 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.")
1045 nom = models.CharField(max_length=255)
1046
1047 class Meta:
1048 ordering = ['code']
1049 verbose_name = u"Statut d'employé"
1050 verbose_name_plural = u"Statuts d'employé"
1051
1052 def __unicode__(self):
1053 return u'%s : %s' % (self.code, self.nom)
1054
1055
1056 TYPE_CLASSEMENT_CHOICES = (
1057 ('S', 'S -Soutien'),
1058 ('T', 'T - Technicien'),
1059 ('P', 'P - Professionel'),
1060 ('C', 'C - Cadre'),
1061 ('D', 'D - Direction'),
1062 ('SO', 'SO - Sans objet [expatriés]'),
1063 ('HG', 'HG - Hors grille [direction]'),
1064 )
1065
1066 class ClassementManager(models.Manager):
1067 """
1068 Ordonner les spcéfiquement les classements.
1069 """
1070 def get_query_set(self):
1071 qs = super(self.__class__, self).get_query_set()
1072 qs = qs.extra(select={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1073 qs = qs.extra(order_by=('ponderation', ))
1074 return qs.all()
1075
1076
1077 class Classement_(AUFMetadata):
1078 """Éléments de classement de la
1079 "Grille générique de classement hiérarchique".
1080
1081 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1082 classement dans la grille. Le classement donne le coefficient utilisé dans:
1083
1084 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1085 """
1086 objects = ClassementManager()
1087
1088 # Identification
1089 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1090 echelon = models.IntegerField(verbose_name=u"Échelon", blank=True, default=0)
1091 degre = models.IntegerField(verbose_name=u"Degré", blank=True, default=0)
1092 coefficient = models.FloatField(default=0, verbose_name=u"Coefficient",
1093 null=True)
1094 # Méta
1095 # annee # au lieu de date_debut et date_fin
1096 commentaire = models.TextField(null=True, blank=True)
1097
1098 class Meta:
1099 abstract = True
1100 ordering = ['type','echelon','degre','coefficient']
1101 verbose_name = u"Classement"
1102 verbose_name_plural = u"Classements"
1103
1104 def __unicode__(self):
1105 return u'%s.%s.%s (%s)' % (self.type, self.echelon, self.degre,
1106 self.coefficient)
1107
1108 class Classement(Classement_):
1109 __doc__ = Classement_.__doc__
1110
1111
1112 class TauxChange_(AUFMetadata):
1113 """Taux de change de la devise vers l'euro (EUR)
1114 pour chaque année budgétaire.
1115 """
1116 # Identification
1117 devise = models.ForeignKey('Devise', db_column='devise')
1118 annee = models.IntegerField(verbose_name = u"Année")
1119 taux = models.FloatField(verbose_name = u"Taux vers l'euro")
1120
1121 class Meta:
1122 abstract = True
1123 ordering = ['-annee', 'devise__code']
1124 verbose_name = u"Taux de change"
1125 verbose_name_plural = u"Taux de change"
1126
1127 def __unicode__(self):
1128 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1129
1130
1131 class TauxChange(TauxChange_):
1132 __doc__ = TauxChange_.__doc__
1133
1134 class ValeurPointManager(NoDeleteManager):
1135
1136 def get_query_set(self):
1137 now = datetime.datetime.now()
1138 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1139
1140
1141 class ValeurPoint_(AUFMetadata):
1142 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1143 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1144 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1145
1146 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1147 """
1148
1149 actuelles = ValeurPointManager()
1150
1151 valeur = models.FloatField(null=True)
1152 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
1153 implantation = models.ForeignKey(ref.Implantation,
1154 db_column='implantation',
1155 related_name='%(app_label)s_valeur_point')
1156 # Méta
1157 annee = models.IntegerField()
1158
1159 class Meta:
1160 ordering = ['-annee', 'implantation__nom']
1161 abstract = True
1162 verbose_name = u"Valeur du point"
1163 verbose_name_plural = u"Valeurs du point"
1164
1165 def __unicode__(self):
1166 return u'%s %s %s [%s] %s' % (self.devise.code, self.annee, self.valeur, self.implantation.nom_court, self.devise.nom)
1167
1168
1169 class ValeurPoint(ValeurPoint_):
1170 __doc__ = ValeurPoint_.__doc__
1171
1172
1173
1174 class Devise(AUFMetadata):
1175 """Devise monétaire.
1176 """
1177
1178 objects = DeviseManager()
1179
1180 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1181 code = models.CharField(max_length=10, unique=True)
1182 nom = models.CharField(max_length=255)
1183
1184 class Meta:
1185 ordering = ['code']
1186 verbose_name = u"Devise"
1187 verbose_name_plural = u"Devises"
1188
1189 def __unicode__(self):
1190 return u'%s - %s' % (self.code, self.nom)
1191
1192 class TypeContrat(AUFMetadata):
1193 """Type de contrat.
1194 """
1195 nom = models.CharField(max_length=255)
1196 nom_long = models.CharField(max_length=255)
1197
1198 class Meta:
1199 ordering = ['nom']
1200 verbose_name = u"Type de contrat"
1201 verbose_name_plural = u"Types de contrat"
1202
1203 def __unicode__(self):
1204 return u'%s' % (self.nom)
1205
1206
1207 ### AUTRES
1208
1209 class ResponsableImplantation(AUFMetadata):
1210 """Le responsable d'une implantation.
1211 Anciennement géré sur le Dossier du responsable.
1212 """
1213 employe = models.ForeignKey('Employe', db_column='employe',
1214 related_name='+',
1215 null=True, blank=True)
1216 implantation = models.ForeignKey(ref.Implantation,
1217 db_column='implantation', related_name='+',
1218 unique=True)
1219
1220 def __unicode__(self):
1221 return u'%s : %s' % (self.implantation, self.employe)
1222
1223 class Meta:
1224 ordering = ['implantation__nom']
1225 verbose_name = "Responsable d'implantation"
1226 verbose_name_plural = "Responsables d'implantation"
1227