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