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