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