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