#2761 rémunération en cours : calculé avec régime de travail
[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 montant_avec_regime(self):
767 return round(self.montant * (self.dossier.regime_travail/100), 2)
768
769 def taux_devise(self):
770 if self.devise.code == "EUR":
771 return 1
772
773 annee = datetime.datetime.now().year
774 if self.dossier.poste.date_debut is not None:
775 annee = self.dossier.poste.date_debut.year
776 if self.dossier.date_debut is not None:
777 annee = self.dossier.date_debut.year
778 if self.date_debut is not None:
779 annee = self.date_debut.year
780
781 taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise_id, annee=annee)]
782 taux = set(taux)
783 if len(taux) != 1:
784 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))
785 else:
786 return list(taux)[0]
787
788 def montant_euro(self):
789 return round(float(self.montant) * float(self.taux_devise()), 2)
790
791 def montant_euro_mois(self):
792 return round(self.montant_euro() / 12, 2)
793
794 def __unicode__(self):
795 try:
796 devise = self.devise.code
797 except:
798 devise = "???"
799 return "%s %s" % (self.montant, devise)
800
801 class Meta:
802 abstract = True
803 verbose_name = u"Rémunération"
804 verbose_name_plural = u"Rémunérations"
805
806
807 class Remuneration(Remuneration_):
808 pass
809
810
811 ### CONTRATS
812
813 class ContratManager(NoDeleteManager):
814 def get_query_set(self):
815 return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste')
816
817
818 class Contrat_(AUFMetadata):
819 """Document juridique qui encadre la relation de travail d'un Employe
820 pour un Poste particulier. Pour un Dossier (qui documente cette
821 relation de travail) plusieurs contrats peuvent être associés.
822 """
823 objects = ContratManager()
824 dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_contrats')
825 type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
826 related_name='+',
827 verbose_name = u"type de contrat")
828 date_debut = models.DateField(verbose_name = u"Date de début")
829 date_fin = models.DateField(verbose_name = u"Date de fin",
830 null=True, blank=True)
831 fichier = models.FileField(verbose_name = u"Fichier",
832 upload_to=contrat_dispatch,
833 storage=storage_prive,
834 null=True, blank=True)
835
836 class Meta:
837 abstract = True
838 ordering = ['dossier__employe__nom']
839 verbose_name = u"Contrat"
840 verbose_name_plural = u"Contrats"
841
842 def __unicode__(self):
843 return u'%s - %s' % (self.dossier, self.id)
844
845 class Contrat(Contrat_):
846 pass
847
848
849 ### ÉVÉNEMENTS
850
851 #class Evenement_(AUFMetadata):
852 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
853 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
854 # (ex.: la Remuneration).
855 #
856 # Ex.: congé de maternité, maladie...
857 #
858 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
859 # différent et une rémunération en conséquence. On souhaite toutefois
860 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
861 # du retour à la normale.
862 # """
863 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
864 # related_name='+')
865 # nom = models.CharField(max_length=255)
866 # date_debut = models.DateField(verbose_name = u"Date de début")
867 # date_fin = models.DateField(verbose_name = u"Date de fin",
868 # null=True, blank=True)
869 #
870 # class Meta:
871 # abstract = True
872 # ordering = ['nom']
873 # verbose_name = u"Évènement"
874 # verbose_name_plural = u"Évènements"
875 #
876 # def __unicode__(self):
877 # return u'%s' % (self.nom)
878 #
879 #
880 #class Evenement(Evenement_):
881 # __doc__ = Evenement_.__doc__
882 #
883 #
884 #class EvenementRemuneration_(RemunerationMixin):
885 # """Structure de rémunération liée à un Evenement qui remplace
886 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
887 # de l'Evenement.
888 # """
889 # evenement = models.ForeignKey("Evenement", db_column='evenement',
890 # related_name='+',
891 # verbose_name = u"Évènement")
892 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
893 # # de l'Evenement associé
894 #
895 # class Meta:
896 # abstract = True
897 # ordering = ['evenement', 'type__nom', '-date_fin']
898 # verbose_name = u"Évènement - rémunération"
899 # verbose_name_plural = u"Évènements - rémunérations"
900 #
901 #
902 #class EvenementRemuneration(EvenementRemuneration_):
903 # __doc__ = EvenementRemuneration_.__doc__
904 #
905 # class Meta:
906 # abstract = True
907 #
908 #
909 #class EvenementRemuneration(EvenementRemuneration_):
910 # __doc__ = EvenementRemuneration_.__doc__
911 # TODO? class ContratPiece(models.Model):
912
913
914 ### RÉFÉRENCES RH
915
916 class FamilleEmploi(AUFMetadata):
917 """Catégorie utilisée dans la gestion des Postes.
918 Catégorie supérieure à TypePoste.
919 """
920 nom = models.CharField(max_length=255)
921
922 class Meta:
923 ordering = ['nom']
924 verbose_name = u"Famille d'emploi"
925 verbose_name_plural = u"Familles d'emploi"
926
927 def __unicode__(self):
928 return u'%s' % (self.nom)
929
930 class TypePoste(AUFMetadata):
931 """Catégorie de Poste.
932 """
933 nom = models.CharField(max_length=255)
934 nom_feminin = models.CharField(max_length=255,
935 verbose_name = u"Nom féminin")
936
937 is_responsable = models.BooleanField(default=False,
938 verbose_name = u"Poste de responsabilité")
939 famille_emploi = models.ForeignKey('FamilleEmploi',
940 db_column='famille_emploi',
941 related_name='+',
942 verbose_name = u"famille d'emploi")
943
944 class Meta:
945 ordering = ['nom']
946 verbose_name = u"Type de poste"
947 verbose_name_plural = u"Types de poste"
948
949 def __unicode__(self):
950 return u'%s' % (self.nom)
951
952
953 TYPE_PAIEMENT_CHOICES = (
954 (u'Régulier', u'Régulier'),
955 (u'Ponctuel', u'Ponctuel'),
956 )
957
958 NATURE_REMUNERATION_CHOICES = (
959 (u'Accessoire', u'Accessoire'),
960 (u'Charges', u'Charges'),
961 (u'Indemnité', u'Indemnité'),
962 (u'RAS', u'Rémunération autre source'),
963 (u'Traitement', u'Traitement'),
964 )
965
966 class TypeRemuneration(AUFMetadata):
967 """Catégorie de Remuneration.
968 """
969 nom = models.CharField(max_length=255)
970 type_paiement = models.CharField(max_length=30,
971 choices=TYPE_PAIEMENT_CHOICES,
972 verbose_name = u"Type de paiement")
973 nature_remuneration = models.CharField(max_length=30,
974 choices=NATURE_REMUNERATION_CHOICES,
975 verbose_name = u"Nature de la rémunération")
976
977 class Meta:
978 ordering = ['nom']
979 verbose_name = u"Type de rémunération"
980 verbose_name_plural = u"Types de rémunération"
981
982 def __unicode__(self):
983 return u'%s' % (self.nom)
984
985 class TypeRevalorisation(AUFMetadata):
986 """Justification du changement de la Remuneration.
987 (Actuellement utilisé dans aucun traitement informatique.)
988 """
989 nom = models.CharField(max_length=255)
990
991 class Meta:
992 ordering = ['nom']
993 verbose_name = u"Type de revalorisation"
994 verbose_name_plural = u"Types de revalorisation"
995
996 def __unicode__(self):
997 return u'%s' % (self.nom)
998
999 class Service(AUFMetadata):
1000 """Unité administrative où les Postes sont rattachés.
1001 """
1002 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1003 nom = models.CharField(max_length=255)
1004
1005 class Meta:
1006 ordering = ['nom']
1007 verbose_name = u"Service"
1008 verbose_name_plural = u"Services"
1009
1010 def __unicode__(self):
1011 if self.archive:
1012 archive = u"(archivé)"
1013 else:
1014 archive = ""
1015 return u'%s %s' % (self.nom, archive)
1016
1017
1018 TYPE_ORGANISME_CHOICES = (
1019 ('MAD', 'Mise à disposition'),
1020 ('DET', 'Détachement'),
1021 )
1022
1023 class OrganismeBstg(AUFMetadata):
1024 """Organisation d'où provient un Employe mis à disposition (MAD) de
1025 ou détaché (DET) à l'AUF à titre gratuit.
1026
1027 (BSTG = bien et service à titre gratuit.)
1028 """
1029 nom = models.CharField(max_length=255)
1030 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
1031 pays = models.ForeignKey(ref.Pays, to_field='code',
1032 db_column='pays',
1033 related_name='organismes_bstg',
1034 null=True, blank=True)
1035
1036 class Meta:
1037 ordering = ['type', 'nom']
1038 verbose_name = u"Organisme BSTG"
1039 verbose_name_plural = u"Organismes BSTG"
1040
1041 def __unicode__(self):
1042 return u'%s (%s)' % (self.nom, self.get_type_display())
1043
1044 prefix_implantation = "pays__region"
1045 def get_regions(self):
1046 return [self.pays.region]
1047
1048
1049 class Statut(AUFMetadata):
1050 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1051 """
1052 # Identification
1053 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.")
1054 nom = models.CharField(max_length=255)
1055
1056 class Meta:
1057 ordering = ['code']
1058 verbose_name = u"Statut d'employé"
1059 verbose_name_plural = u"Statuts d'employé"
1060
1061 def __unicode__(self):
1062 return u'%s : %s' % (self.code, self.nom)
1063
1064
1065 TYPE_CLASSEMENT_CHOICES = (
1066 ('S', 'S -Soutien'),
1067 ('T', 'T - Technicien'),
1068 ('P', 'P - Professionel'),
1069 ('C', 'C - Cadre'),
1070 ('D', 'D - Direction'),
1071 ('SO', 'SO - Sans objet [expatriés]'),
1072 ('HG', 'HG - Hors grille [direction]'),
1073 )
1074
1075 class ClassementManager(models.Manager):
1076 """
1077 Ordonner les spcéfiquement les classements.
1078 """
1079 def get_query_set(self):
1080 qs = super(self.__class__, self).get_query_set()
1081 qs = qs.extra(select={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1082 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
1083 return qs.all()
1084
1085
1086 class Classement_(AUFMetadata):
1087 """Éléments de classement de la
1088 "Grille générique de classement hiérarchique".
1089
1090 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1091 classement dans la grille. Le classement donne le coefficient utilisé dans:
1092
1093 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1094 """
1095 objects = ClassementManager()
1096
1097 # Identification
1098 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
1099 echelon = models.IntegerField(verbose_name=u"Échelon", blank=True, default=0)
1100 degre = models.IntegerField(verbose_name=u"Degré", blank=True, default=0)
1101 coefficient = models.FloatField(default=0, verbose_name=u"Coefficient",
1102 null=True)
1103 # Méta
1104 # annee # au lieu de date_debut et date_fin
1105 commentaire = models.TextField(null=True, blank=True)
1106
1107 class Meta:
1108 abstract = True
1109 ordering = ['type','echelon','degre','coefficient']
1110 verbose_name = u"Classement"
1111 verbose_name_plural = u"Classements"
1112
1113 def __unicode__(self):
1114 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
1115
1116 class Classement(Classement_):
1117 __doc__ = Classement_.__doc__
1118
1119
1120 class TauxChange_(AUFMetadata):
1121 """Taux de change de la devise vers l'euro (EUR)
1122 pour chaque année budgétaire.
1123 """
1124 # Identification
1125 devise = models.ForeignKey('Devise', db_column='devise')
1126 annee = models.IntegerField(verbose_name = u"Année")
1127 taux = models.FloatField(verbose_name = u"Taux vers l'euro")
1128
1129 class Meta:
1130 abstract = True
1131 ordering = ['-annee', 'devise__code']
1132 verbose_name = u"Taux de change"
1133 verbose_name_plural = u"Taux de change"
1134
1135 def __unicode__(self):
1136 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
1137
1138
1139 class TauxChange(TauxChange_):
1140 __doc__ = TauxChange_.__doc__
1141
1142 class ValeurPointManager(NoDeleteManager):
1143
1144 def get_query_set(self):
1145 return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
1146
1147
1148 class ValeurPoint_(AUFMetadata):
1149 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1150 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1151 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1152
1153 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1154 """
1155
1156 actuelles = ValeurPointManager()
1157
1158 valeur = models.FloatField(null=True)
1159 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
1160 implantation = models.ForeignKey(ref.Implantation,
1161 db_column='implantation',
1162 related_name='%(app_label)s_valeur_point')
1163 # Méta
1164 annee = models.IntegerField()
1165
1166 class Meta:
1167 ordering = ['-annee', 'implantation__nom']
1168 abstract = True
1169 verbose_name = u"Valeur du point"
1170 verbose_name_plural = u"Valeurs du point"
1171
1172 def __unicode__(self):
1173 return u'%s %s %s [%s] %s' % (self.devise.code, self.annee, self.valeur, self.implantation.nom_court, self.devise.nom)
1174
1175
1176 class ValeurPoint(ValeurPoint_):
1177 __doc__ = ValeurPoint_.__doc__
1178
1179
1180
1181 class Devise(AUFMetadata):
1182 """Devise monétaire.
1183 """
1184
1185 objects = DeviseManager()
1186
1187 archive = models.BooleanField(verbose_name=u"Archivé", default=False)
1188 code = models.CharField(max_length=10, unique=True)
1189 nom = models.CharField(max_length=255)
1190
1191 class Meta:
1192 ordering = ['code']
1193 verbose_name = u"Devise"
1194 verbose_name_plural = u"Devises"
1195
1196 def __unicode__(self):
1197 return u'%s - %s' % (self.code, self.nom)
1198
1199 class TypeContrat(AUFMetadata):
1200 """Type de contrat.
1201 """
1202 nom = models.CharField(max_length=255)
1203 nom_long = models.CharField(max_length=255)
1204
1205 class Meta:
1206 ordering = ['nom']
1207 verbose_name = u"Type de contrat"
1208 verbose_name_plural = u"Types de contrat"
1209
1210 def __unicode__(self):
1211 return u'%s' % (self.nom)
1212
1213
1214 ### AUTRES
1215
1216 class ResponsableImplantation(AUFMetadata):
1217 """Le responsable d'une implantation.
1218 Anciennement géré sur le Dossier du responsable.
1219 """
1220 employe = models.ForeignKey('Employe', db_column='employe',
1221 related_name='+',
1222 null=True, blank=True)
1223 implantation = models.ForeignKey(ref.Implantation,
1224 db_column='implantation', related_name='+',
1225 unique=True)
1226
1227 def __unicode__(self):
1228 return u'%s : %s' % (self.implantation, self.employe)
1229
1230 class Meta:
1231 ordering = ['implantation__nom']
1232 verbose_name = "Responsable d'implantation"
1233 verbose_name_plural = "Responsables d'implantation"
1234