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