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