1 # -=- encoding: utf-8 -=-
4 from datetime
import date
5 from decimal
import Decimal
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 from django
.db
.models
import Q
13 from auf
.django
.emploi
.models
import GENRE_CHOICES
, SITUATION_CHOICES
# devrait plutot être dans references
14 from auf
.django
.metadata
.models
import AUFMetadata
15 from auf
.django
.metadata
.managers
import NoDeleteManager
16 import auf
.django
.references
.models
as ref
17 from validators
import validate_date_passee
18 from managers
import PosteManager
, DossierManager
, DossierComparaisonManager
, PosteComparaisonManager
, DeviseManager
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
25 models_stack
= [s
[1].split('/')[-2] for s
in inspect
.stack() if s
[1].endswith('models.py')]
26 return models_stack
[-1]
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')
35 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
36 base_url
=settings
.PRIVE_MEDIA_URL
)
38 def poste_piece_dispatch(instance
, filename
):
39 path
= "%s/poste/%s/%s" % (instance
._meta
.app_label
, instance
.poste_id
, filename
)
42 def dossier_piece_dispatch(instance
, filename
):
43 path
= "%s/dossier/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
46 def employe_piece_dispatch(instance
, filename
):
47 path
= "%s/employe/%s/%s" % (instance
._meta
.app_label
, instance
.employe_id
, filename
)
50 def contrat_dispatch(instance
, filename
):
51 path
= "%s/contrat/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
55 class Commentaire(AUFMetadata
):
56 texte
= models
.TextField()
57 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
61 ordering
= ['-date_creation']
63 def __unicode__(self
):
64 return u
'%s' % (self
.texte
)
69 POSTE_APPEL_CHOICES
= (
70 ('interne', 'Interne'),
71 ('externe', 'Externe'),
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.
80 objects
= PosteManager()
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)",
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",
93 verbose_name
=u
"type de poste")
94 service
= models
.ForeignKey('Service', db_column
='service',
96 verbose_name
= u
"direction/service/pôle support",
98 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
101 help_text
=u
"Taper le nom du poste ou du type de poste",
102 verbose_name
= u
"Poste du responsable", )
105 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
106 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
107 verbose_name
= u
"Temps de travail",
108 help_text
="% du temps complet")
109 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
110 decimal_places
=2, null
=True,
111 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
112 verbose_name
= u
"Nb. heures par semaine")
115 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
116 null
=True, blank
=True)
117 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
118 null
=True, blank
=True)
119 mise_a_disposition
= models
.NullBooleanField(
120 verbose_name
= u
"Mise à disposition",
121 null
=True, default
=False)
122 appel
= models
.CharField(max_length
=10, null
=True,
123 verbose_name
= u
"Appel à candidature",
124 choices
=POSTE_APPEL_CHOICES
,
128 classement_min
= models
.ForeignKey('Classement',
129 db_column
='classement_min', related_name
='+',
130 null
=True, blank
=True)
131 classement_max
= models
.ForeignKey('Classement',
132 db_column
='classement_max', related_name
='+',
133 null
=True, blank
=True)
134 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
135 db_column
='valeur_point_min', related_name
='+',
136 null
=True, blank
=True)
137 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
138 db_column
='valeur_point_max', related_name
='+',
139 null
=True, blank
=True)
140 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
142 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
144 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
145 null
=True, default
=0)
146 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 null
=True, default
=0)
148 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
149 null
=True, default
=0)
150 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
151 null
=True, default
=0)
152 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
153 null
=True, default
=0)
154 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
155 null
=True, default
=0)
157 # Comparatifs de rémunération
158 devise_comparaison
= models
.ForeignKey('Devise', null
=True, blank
=True,
159 db_column
='devise_comparaison',
161 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
162 null
=True, blank
=True)
163 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
164 null
=True, blank
=True)
165 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
166 null
=True, blank
=True)
167 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
168 null
=True, blank
=True)
169 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
170 null
=True, blank
=True)
171 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
172 null
=True, blank
=True)
173 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
174 null
=True, blank
=True)
175 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
176 null
=True, blank
=True)
177 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
178 null
=True, blank
=True)
179 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
180 null
=True, blank
=True)
183 justification
= models
.TextField(null
=True, blank
=True)
186 date_debut
= models
.DateField(verbose_name
=u
"Date de début", help_text
=HELP_TEXT_DATE
,
187 null
=True, blank
=True)
188 date_fin
= models
.DateField(verbose_name
=u
"Date de fin", help_text
=HELP_TEXT_DATE
,
189 null
=True, blank
=True)
193 ordering
= ['implantation__nom', 'nom']
194 verbose_name
= u
"Poste"
195 verbose_name_plural
= u
"Postes"
198 def __unicode__(self
):
199 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
201 return representation
203 prefix_implantation
= "implantation__region"
204 def get_regions(self
):
205 return [self
.implantation
.region
]
209 __doc__
= Poste_
.__doc__
211 # meta dématérialisation : pour permettre le filtrage
212 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
216 if self
.occupe_par():
220 def occupe_par(self
):
221 """Retourne la liste d'employé occupant ce poste.
222 Généralement, retourne une liste d'un élément.
223 Si poste inoccupé, retourne liste vide.
224 UTILISE pour mettre a jour le flag vacant
226 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
229 POSTE_FINANCEMENT_CHOICES
= (
230 ('A', 'A - Frais de personnel'),
231 ('B', 'B - Projet(s)-Titre(s)'),
236 class PosteFinancement_(models
.Model
):
237 """Pour un Poste, structure d'informations décrivant comment on prévoit
240 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', 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.")
251 def __unicode__(self
):
252 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
255 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
258 class PosteFinancement(PosteFinancement_
):
262 class PostePiece_(models
.Model
):
263 """Documents relatifs au Poste.
264 Ex.: Description de poste
266 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
267 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
268 fichier
= models
.FileField(verbose_name
= u
"Fichier",
269 upload_to
=poste_piece_dispatch
,
270 storage
=storage_prive
)
276 def __unicode__(self
):
277 return u
'%s' % (self
.nom
)
279 class PostePiece(PostePiece_
):
282 class PosteComparaison_(AUFMetadata
):
284 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
286 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
287 objects
= PosteComparaisonManager()
289 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
290 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
291 montant
= models
.IntegerField(null
=True)
292 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
297 def taux_devise(self
):
298 if self
.devise
.code
== "EUR":
300 annee
= self
.poste
.date_debut
.year
301 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
304 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
))
308 def montant_euros(self
):
309 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
311 def __unicode__(self
):
314 class PosteComparaison(PosteComparaison_
):
317 class PosteCommentaire_(Commentaire
):
318 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
323 class PosteCommentaire(PosteCommentaire_
):
328 class Employe(AUFMetadata
):
329 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
330 Dossiers qu'il occupe ou a occupé de Postes.
332 Cette classe aurait pu avantageusement s'appeler Personne car la notion
333 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
336 nom
= models
.CharField(max_length
=255)
337 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
338 nom_affichage
= models
.CharField(max_length
=255,
339 verbose_name
= u
"Nom d'affichage",
340 null
=True, blank
=True)
341 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
342 db_column
='nationalite',
343 related_name
='employes_nationalite',
344 verbose_name
= u
"Nationalité",
345 blank
=True, null
=True)
346 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
347 help_text
=HELP_TEXT_DATE
,
348 validators
=[validate_date_passee
],
349 null
=True, blank
=True)
350 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
353 situation_famille
= models
.CharField(max_length
=1,
354 choices
=SITUATION_CHOICES
,
355 verbose_name
= u
"Situation familiale",
356 null
=True, blank
=True)
357 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
358 help_text
=HELP_TEXT_DATE
,
359 null
=True, blank
=True)
362 tel_domicile
= models
.CharField(max_length
=255,
363 verbose_name
= u
"Tél. domicile",
364 null
=True, blank
=True)
365 tel_cellulaire
= models
.CharField(max_length
=255,
366 verbose_name
= u
"Tél. cellulaire",
367 null
=True, blank
=True)
368 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
369 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
370 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
371 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
372 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
373 related_name
='employes',
374 null
=True, blank
=True)
376 # meta dématérialisation : pour permettre le filtrage
377 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
380 ordering
= ['nom','prenom']
381 verbose_name
= u
"Employé"
382 verbose_name_plural
= u
"Employés"
384 def __unicode__(self
):
385 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
389 if self
.genre
.upper() == u
'M':
391 elif self
.genre
.upper() == u
'F':
396 """Retourne l'URL du service retournant la photo de l'Employe.
397 Équivalent reverse url 'rh_photo' avec id en param.
399 from django
.core
.urlresolvers
import reverse
400 return reverse('rh_photo', kwargs
={'id':self
.id})
402 def dossiers_passes(self
):
404 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
405 for d
in dossiers_passes
:
407 return dossiers_passes
409 def dossiers_futurs(self
):
411 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
413 def dossiers_encours(self
):
414 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
415 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
416 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
418 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
419 for d
in dossiers_encours
:
420 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
421 return dossiers_encours
423 def postes_encours(self
):
424 postes_encours
= set()
425 for d
in self
.dossiers_encours():
426 postes_encours
.add(d
.poste
)
427 return postes_encours
429 def poste_principal(self
):
431 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
433 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
435 poste
= Poste
.objects
.none()
437 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
442 prefix_implantation
= "rh_dossiers__poste__implantation__region"
443 def get_regions(self
):
445 for d
in self
.dossiers
.all():
446 regions
.append(d
.poste
.implantation
.region
)
450 class EmployePiece(models
.Model
):
451 """Documents relatifs à un employé.
454 employe
= models
.ForeignKey('Employe', db_column
='employe')
455 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
456 fichier
= models
.FileField(verbose_name
="Fichier",
457 upload_to
=employe_piece_dispatch
,
458 storage
=storage_prive
)
462 verbose_name
= u
"Employé pièce"
463 verbose_name_plural
= u
"Employé pièces"
465 def __unicode__(self
):
466 return u
'%s' % (self
.nom
)
468 class EmployeCommentaire(Commentaire
):
469 employe
= models
.ForeignKey('Employe', db_column
='employe',
473 verbose_name
= u
"Employé commentaire"
474 verbose_name_plural
= u
"Employé commentaires"
476 class EmployeProxy(Employe
):
480 verbose_name
= u
"Organigramme des employés"
481 verbose_name_plural
= u
"Organigramme des employés"
483 LIEN_PARENTE_CHOICES
= (
484 ('Conjoint', 'Conjoint'),
485 ('Conjointe', 'Conjointe'),
490 class AyantDroit(AUFMetadata
):
491 """Personne en relation avec un Employe.
494 nom
= models
.CharField(max_length
=255)
495 prenom
= models
.CharField(max_length
=255,
496 verbose_name
= u
"Prénom",)
497 nom_affichage
= models
.CharField(max_length
=255,
498 verbose_name
= u
"Nom d'affichage",
499 null
=True, blank
=True)
500 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
501 db_column
='nationalite',
502 related_name
='ayantdroits_nationalite',
503 verbose_name
= u
"Nationalité",
504 null
=True, blank
=True)
505 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
506 help_text
=HELP_TEXT_DATE
,
507 validators
=[validate_date_passee
],
508 null
=True, blank
=True)
509 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
512 employe
= models
.ForeignKey('Employe', db_column
='employe',
513 related_name
='ayantdroits',
514 verbose_name
= u
"Employé")
515 lien_parente
= models
.CharField(max_length
=10,
516 choices
=LIEN_PARENTE_CHOICES
,
517 verbose_name
= u
"Lien de parenté",
518 null
=True, blank
=True)
522 verbose_name
= u
"Ayant droit"
523 verbose_name_plural
= u
"Ayants droit"
525 def __unicode__(self
):
526 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
528 prefix_implantation
= "employe__dossiers__poste__implantation__region"
529 def get_regions(self
):
531 for d
in self
.employe
.dossiers
.all():
532 regions
.append(d
.poste
.implantation
.region
)
536 class AyantDroitCommentaire(Commentaire
):
537 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
543 STATUT_RESIDENCE_CHOICES
= (
545 ('expat', 'Expatrié'),
548 COMPTE_COMPTA_CHOICES
= (
554 class Dossier_(AUFMetadata
):
555 """Le Dossier regroupe les informations relatives à l'occupation
556 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
559 Plusieurs Contrats peuvent être associés au Dossier.
560 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
561 lequel aucun Dossier n'existe est un poste vacant.
564 objects
= DossierManager()
567 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
568 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
569 db_column
='organisme_bstg',
571 verbose_name
= u
"Organisme",
572 help_text
="Si détaché (DET) ou \
573 mis à disposition (MAD), \
574 préciser l'organisme.",
575 null
=True, blank
=True)
578 remplacement
= models
.BooleanField(default
=False)
579 remplacement_de
= models
.ForeignKey('self', related_name
='+',
580 help_text
=u
"Taper le nom de l'employé",
581 null
=True, blank
=True)
582 statut_residence
= models
.CharField(max_length
=10, default
='local',
583 verbose_name
= u
"Statut", null
=True,
584 choices
=STATUT_RESIDENCE_CHOICES
)
587 classement
= models
.ForeignKey('Classement', db_column
='classement',
589 null
=True, blank
=True)
590 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
592 default
=REGIME_TRAVAIL_DEFAULT
,
593 verbose_name
= u
"Régime de travail",
594 help_text
="% du temps complet")
595 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
596 decimal_places
=2, null
=True,
597 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
598 verbose_name
= u
"Nb. heures par semaine")
600 # Occupation du Poste par cet Employe (anciennement "mandat")
601 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
603 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
605 null
=True, blank
=True)
612 ordering
= ['employe__nom', ]
613 verbose_name
= u
"Dossier"
614 verbose_name_plural
= "Dossiers"
616 def salaire_theorique(self
):
617 annee
= date
.today().year
618 coeff
= self
.classement
.coefficient
619 implantation
= self
.poste
.implantation
620 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
622 montant
= coeff
* point
.valeur
623 devise
= point
.devise
624 return {'montant':montant
, 'devise':devise
}
626 def __unicode__(self
):
627 poste
= self
.poste
.nom
628 if self
.employe
.genre
== 'F':
629 poste
= self
.poste
.nom_feminin
630 return u
'%s - %s' % (self
.employe
, poste
)
632 prefix_implantation
= "poste__implantation__region"
633 def get_regions(self
):
634 return [self
.poste
.implantation
.region
]
637 def remunerations(self
):
638 return self
.rh_remunerations
.all().order_by('date_debut')
640 def remunerations_en_cours(self
):
641 return self
.rh_remunerations
.all().filter(date_fin__exact
=None).order_by('date_debut')
643 def get_salaire(self
):
645 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
649 class Dossier(Dossier_
):
650 __doc__
= Dossier_
.__doc__
651 poste
= models
.ForeignKey('%s.Poste' % app_context(),
653 related_name
='%(app_label)s_dossiers',
654 help_text
=u
"Taper le nom du poste ou du type de poste",
656 employe
= models
.ForeignKey('Employe', db_column
='employe',
657 help_text
=u
"Taper le nom de l'employé",
658 related_name
='%(app_label)s_dossiers',
659 verbose_name
=u
"Employé")
662 class DossierPiece_(models
.Model
):
663 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
664 Ex.: Lettre de motivation.
666 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
667 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
668 fichier
= models
.FileField(verbose_name
= u
"Fichier",
669 upload_to
=dossier_piece_dispatch
,
670 storage
=storage_prive
)
676 def __unicode__(self
):
677 return u
'%s' % (self
.nom
)
679 class DossierPiece(DossierPiece_
):
682 class DossierCommentaire_(Commentaire
):
683 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
687 class DossierCommentaire(DossierCommentaire_
):
690 class DossierComparaison_(models
.Model
):
692 Photo d'une comparaison salariale au moment de l'embauche.
694 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
695 objects
= DossierComparaisonManager()
697 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
698 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
699 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
700 montant
= models
.IntegerField(null
=True)
701 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
706 def taux_devise(self
):
707 annee
= self
.dossier
.poste
.date_debut
.year
708 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
711 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
))
715 def montant_euros(self
):
716 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
718 class DossierComparaison(DossierComparaison_
):
723 class RemunerationMixin(AUFMetadata
):
724 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
726 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
728 verbose_name
= u
"Type de rémunération")
729 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
730 db_column
='type_revalorisation',
732 verbose_name
= u
"Type de revalorisation",
733 null
=True, blank
=True)
734 montant
= models
.DecimalField(null
=True, blank
=True,
735 default
=0, max_digits
=12, decimal_places
=2)
736 # Annuel (12 mois, 52 semaines, 364 jours?)
737 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
738 # commentaire = precision
739 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
740 # date_debut = anciennement date_effectif
741 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
742 null
=True, blank
=True)
743 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
744 null
=True, blank
=True)
748 ordering
= ['type__nom', '-date_fin']
750 def __unicode__(self
):
751 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
753 class Remuneration_(RemunerationMixin
):
754 """Structure de rémunération (données budgétaires) en situation normale
755 pour un Dossier. Si un Evenement existe, utiliser la structure de
756 rémunération EvenementRemuneration de cet événement.
759 def montant_mois(self
):
760 return round(self
.montant
/ 12, 2)
762 def taux_devise(self
):
763 if self
.devise
.code
== "EUR":
766 annee
= datetime
.datetime
.now().year
767 if self
.date_debut
is not None:
768 annee
= self
.date_debut
.year
769 if self
.dossier
.poste
.date_debut
is not None:
770 annee
= self
.dossier
.poste
.date_debut
.year
772 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
775 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
))
779 def montant_euro(self
):
780 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
782 def montant_euro_mois(self
):
783 return round(self
.montant_euro() / 12, 2)
785 def __unicode__(self
):
787 devise
= self
.devise
.code
790 return "%s %s" % (self
.montant
, devise
)
794 verbose_name
= u
"Rémunération"
795 verbose_name_plural
= u
"Rémunérations"
798 class Remuneration(Remuneration_
):
804 class ContratManager(NoDeleteManager
):
805 def get_query_set(self
):
806 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
809 class Contrat_(AUFMetadata
):
810 """Document juridique qui encadre la relation de travail d'un Employe
811 pour un Poste particulier. Pour un Dossier (qui documente cette
812 relation de travail) plusieurs contrats peuvent être associés.
814 objects
= ContratManager()
815 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
816 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
818 verbose_name
= u
"type de contrat")
819 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
820 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
821 null
=True, blank
=True)
822 fichier
= models
.FileField(verbose_name
= u
"Fichier",
823 upload_to
=contrat_dispatch
,
824 storage
=storage_prive
,
825 null
=True, blank
=True)
829 ordering
= ['dossier__employe__nom']
830 verbose_name
= u
"Contrat"
831 verbose_name_plural
= u
"Contrats"
833 def __unicode__(self
):
834 return u
'%s - %s' % (self
.dossier
, self
.id)
836 class Contrat(Contrat_
):
842 #class Evenement_(AUFMetadata):
843 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
844 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
845 # (ex.: la Remuneration).
847 # Ex.: congé de maternité, maladie...
849 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
850 # différent et une rémunération en conséquence. On souhaite toutefois
851 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
852 # du retour à la normale.
854 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
856 # nom = models.CharField(max_length=255)
857 # date_debut = models.DateField(verbose_name = u"Date de début")
858 # date_fin = models.DateField(verbose_name = u"Date de fin",
859 # null=True, blank=True)
864 # verbose_name = u"Évènement"
865 # verbose_name_plural = u"Évènements"
867 # def __unicode__(self):
868 # return u'%s' % (self.nom)
871 #class Evenement(Evenement_):
872 # __doc__ = Evenement_.__doc__
875 #class EvenementRemuneration_(RemunerationMixin):
876 # """Structure de rémunération liée à un Evenement qui remplace
877 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
880 # evenement = models.ForeignKey("Evenement", db_column='evenement',
882 # verbose_name = u"Évènement")
883 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
884 # # de l'Evenement associé
888 # ordering = ['evenement', 'type__nom', '-date_fin']
889 # verbose_name = u"Évènement - rémunération"
890 # verbose_name_plural = u"Évènements - rémunérations"
893 #class EvenementRemuneration(EvenementRemuneration_):
894 # __doc__ = EvenementRemuneration_.__doc__
900 #class EvenementRemuneration(EvenementRemuneration_):
901 # __doc__ = EvenementRemuneration_.__doc__
902 # TODO? class ContratPiece(models.Model):
907 class FamilleEmploi(AUFMetadata
):
908 """Catégorie utilisée dans la gestion des Postes.
909 Catégorie supérieure à TypePoste.
911 nom
= models
.CharField(max_length
=255)
915 verbose_name
= u
"Famille d'emploi"
916 verbose_name_plural
= u
"Familles d'emploi"
918 def __unicode__(self
):
919 return u
'%s' % (self
.nom
)
921 class TypePoste(AUFMetadata
):
922 """Catégorie de Poste.
924 nom
= models
.CharField(max_length
=255)
925 nom_feminin
= models
.CharField(max_length
=255,
926 verbose_name
= u
"Nom féminin")
928 is_responsable
= models
.BooleanField(default
=False,
929 verbose_name
= u
"Poste de responsabilité")
930 famille_emploi
= models
.ForeignKey('FamilleEmploi',
931 db_column
='famille_emploi',
933 verbose_name
= u
"famille d'emploi")
937 verbose_name
= u
"Type de poste"
938 verbose_name_plural
= u
"Types de poste"
940 def __unicode__(self
):
941 return u
'%s' % (self
.nom
)
944 TYPE_PAIEMENT_CHOICES
= (
945 (u
'Régulier', u
'Régulier'),
946 (u
'Ponctuel', u
'Ponctuel'),
949 NATURE_REMUNERATION_CHOICES
= (
950 (u
'Accessoire', u
'Accessoire'),
951 (u
'Charges', u
'Charges'),
952 (u
'Indemnité', u
'Indemnité'),
953 (u
'RAS', u
'Rémunération autre source'),
954 (u
'Traitement', u
'Traitement'),
957 class TypeRemuneration(AUFMetadata
):
958 """Catégorie de Remuneration.
960 nom
= models
.CharField(max_length
=255)
961 type_paiement
= models
.CharField(max_length
=30,
962 choices
=TYPE_PAIEMENT_CHOICES
,
963 verbose_name
= u
"Type de paiement")
964 nature_remuneration
= models
.CharField(max_length
=30,
965 choices
=NATURE_REMUNERATION_CHOICES
,
966 verbose_name
= u
"Nature de la rémunération")
970 verbose_name
= u
"Type de rémunération"
971 verbose_name_plural
= u
"Types de rémunération"
973 def __unicode__(self
):
974 return u
'%s' % (self
.nom
)
976 class TypeRevalorisation(AUFMetadata
):
977 """Justification du changement de la Remuneration.
978 (Actuellement utilisé dans aucun traitement informatique.)
980 nom
= models
.CharField(max_length
=255)
984 verbose_name
= u
"Type de revalorisation"
985 verbose_name_plural
= u
"Types de revalorisation"
987 def __unicode__(self
):
988 return u
'%s' % (self
.nom
)
990 class Service(AUFMetadata
):
991 """Unité administrative où les Postes sont rattachés.
993 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
994 nom
= models
.CharField(max_length
=255)
998 verbose_name
= u
"Service"
999 verbose_name_plural
= u
"Services"
1001 def __unicode__(self
):
1003 archive
= u
"(archivé)"
1006 return u
'%s %s' % (self
.nom
, archive
)
1009 TYPE_ORGANISME_CHOICES
= (
1010 ('MAD', 'Mise à disposition'),
1011 ('DET', 'Détachement'),
1014 class OrganismeBstg(AUFMetadata
):
1015 """Organisation d'où provient un Employe mis à disposition (MAD) de
1016 ou détaché (DET) à l'AUF à titre gratuit.
1018 (BSTG = bien et service à titre gratuit.)
1020 nom
= models
.CharField(max_length
=255)
1021 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1022 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1024 related_name
='organismes_bstg',
1025 null
=True, blank
=True)
1028 ordering
= ['type', 'nom']
1029 verbose_name
= u
"Organisme BSTG"
1030 verbose_name_plural
= u
"Organismes BSTG"
1032 def __unicode__(self
):
1033 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1035 prefix_implantation
= "pays__region"
1036 def get_regions(self
):
1037 return [self
.pays
.region
]
1040 class Statut(AUFMetadata
):
1041 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1044 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.")
1045 nom
= models
.CharField(max_length
=255)
1049 verbose_name
= u
"Statut d'employé"
1050 verbose_name_plural
= u
"Statuts d'employé"
1052 def __unicode__(self
):
1053 return u
'%s : %s' % (self
.code
, self
.nom
)
1056 TYPE_CLASSEMENT_CHOICES
= (
1057 ('S', 'S -Soutien'),
1058 ('T', 'T - Technicien'),
1059 ('P', 'P - Professionel'),
1061 ('D', 'D - Direction'),
1062 ('SO', 'SO - Sans objet [expatriés]'),
1063 ('HG', 'HG - Hors grille [direction]'),
1066 class ClassementManager(models
.Manager
):
1068 Ordonner les spcéfiquement les classements.
1070 def get_query_set(self
):
1071 qs
= super(self
.__class__
, self
).get_query_set()
1072 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1073 qs
= qs
.extra(order_by
=('ponderation', ))
1077 class Classement_(AUFMetadata
):
1078 """Éléments de classement de la
1079 "Grille générique de classement hiérarchique".
1081 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1082 classement dans la grille. Le classement donne le coefficient utilisé dans:
1084 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1086 objects
= ClassementManager()
1089 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1090 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1091 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1092 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1095 # annee # au lieu de date_debut et date_fin
1096 commentaire
= models
.TextField(null
=True, blank
=True)
1100 ordering
= ['type','echelon','degre','coefficient']
1101 verbose_name
= u
"Classement"
1102 verbose_name_plural
= u
"Classements"
1104 def __unicode__(self
):
1105 return u
'%s.%s.%s (%s)' % (self
.type, self
.echelon
, self
.degre
,
1108 class Classement(Classement_
):
1109 __doc__
= Classement_
.__doc__
1112 class TauxChange_(AUFMetadata
):
1113 """Taux de change de la devise vers l'euro (EUR)
1114 pour chaque année budgétaire.
1117 devise
= models
.ForeignKey('Devise', db_column
='devise')
1118 annee
= models
.IntegerField(verbose_name
= u
"Année")
1119 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1123 ordering
= ['-annee', 'devise__code']
1124 verbose_name
= u
"Taux de change"
1125 verbose_name_plural
= u
"Taux de change"
1127 def __unicode__(self
):
1128 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1131 class TauxChange(TauxChange_
):
1132 __doc__
= TauxChange_
.__doc__
1134 class ValeurPointManager(NoDeleteManager
):
1136 def get_query_set(self
):
1137 now
= datetime
.datetime
.now()
1138 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1141 class ValeurPoint_(AUFMetadata
):
1142 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1143 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1144 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1146 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1149 actuelles
= ValeurPointManager()
1151 valeur
= models
.FloatField(null
=True)
1152 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1153 implantation
= models
.ForeignKey(ref
.Implantation
,
1154 db_column
='implantation',
1155 related_name
='%(app_label)s_valeur_point')
1157 annee
= models
.IntegerField()
1160 ordering
= ['-annee', 'implantation__nom']
1162 verbose_name
= u
"Valeur du point"
1163 verbose_name_plural
= u
"Valeurs du point"
1165 def __unicode__(self
):
1166 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1169 class ValeurPoint(ValeurPoint_
):
1170 __doc__
= ValeurPoint_
.__doc__
1174 class Devise(AUFMetadata
):
1175 """Devise monétaire.
1178 objects
= DeviseManager()
1180 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1181 code
= models
.CharField(max_length
=10, unique
=True)
1182 nom
= models
.CharField(max_length
=255)
1186 verbose_name
= u
"Devise"
1187 verbose_name_plural
= u
"Devises"
1189 def __unicode__(self
):
1190 return u
'%s - %s' % (self
.code
, self
.nom
)
1192 class TypeContrat(AUFMetadata
):
1195 nom
= models
.CharField(max_length
=255)
1196 nom_long
= models
.CharField(max_length
=255)
1200 verbose_name
= u
"Type de contrat"
1201 verbose_name_plural
= u
"Types de contrat"
1203 def __unicode__(self
):
1204 return u
'%s' % (self
.nom
)
1209 class ResponsableImplantation(AUFMetadata
):
1210 """Le responsable d'une implantation.
1211 Anciennement géré sur le Dossier du responsable.
1213 employe
= models
.ForeignKey('Employe', db_column
='employe',
1215 null
=True, blank
=True)
1216 implantation
= models
.ForeignKey(ref
.Implantation
,
1217 db_column
='implantation', related_name
='+',
1220 def __unicode__(self
):
1221 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1224 ordering
= ['implantation__nom']
1225 verbose_name
= "Responsable d'implantation"
1226 verbose_name_plural
= "Responsables d'implantation"