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