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