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