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