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