1 # -=- encoding: utf-8 -=-
4 from datetime
import date
5 from decimal
import Decimal
7 from django
.core
.files
.storage
import FileSystemStorage
8 from django
.db
import models
9 from django
.db
.models
import Q
10 from django
.conf
import settings
12 from auf
.django
.emploi
.models
import GENRE_CHOICES
, SITUATION_CHOICES
# devrait plutot être dans references
13 from auf
.django
.metadata
.models
import AUFMetadata
14 from auf
.django
.metadata
.managers
import NoDeleteManager
15 import auf
.django
.references
.models
as ref
16 from validators
import validate_date_passee
17 from managers
import PosteManager
, DossierManager
, DossierComparaisonManager
, \
18 PosteComparaisonManager
, DeviseManager
, ServiceManager
, TypeRemunerationManager
19 from change_list
import RechercheTemporelle
, KEY_STATUT
, STATUT_ACTIF
, \
20 STATUT_INACTIF
, STATUT_FUTUR
23 # Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
24 # Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
27 models_stack
= [s
[1].split('/')[-2] for s
in inspect
.stack() if s
[1].endswith('models.py')]
28 return models_stack
[-1]
32 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
33 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
34 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
35 REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
= "Saisir le nombre d'heure de travail à temps complet (100%), sans tenir compte du régime de travail"
38 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
39 base_url
=settings
.PRIVE_MEDIA_URL
)
41 def poste_piece_dispatch(instance
, filename
):
42 path
= "%s/poste/%s/%s" % (instance
._meta
.app_label
, instance
.poste_id
, filename
)
45 def dossier_piece_dispatch(instance
, filename
):
46 path
= "%s/dossier/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
49 def employe_piece_dispatch(instance
, filename
):
50 path
= "%s/employe/%s/%s" % (instance
._meta
.app_label
, instance
.employe_id
, filename
)
53 def contrat_dispatch(instance
, filename
):
54 path
= "%s/contrat/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
58 class DevisableMixin(object):
60 def get_annee_pour_taux_devise(self
):
61 return datetime
.datetime
.now().year
64 def taux_devise(self
, devise
=None):
70 if devise
.code
== "EUR":
73 annee
= self
.get_annee_pour_taux_devise()
74 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=devise
, annee
=annee
)]
78 raise Exception(u
"Pas de taux pour %s en %s" % (devise
.code
, annee
))
81 raise Exception(u
"Il existe plusieurs taux de %s en %s" %
86 def montant_euros(self
):
88 taux
= self
.taux_devise()
93 return int(round(float(self
.montant
) * float(taux
), 2))
96 class Commentaire(AUFMetadata
):
97 texte
= models
.TextField()
98 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
102 ordering
= ['-date_creation']
104 def __unicode__(self
):
105 return u
'%s' % (self
.texte
)
110 POSTE_APPEL_CHOICES
= (
111 ('interne', 'Interne'),
112 ('externe', 'Externe'),
115 class Poste_(AUFMetadata
):
116 """Un Poste est un emploi (job) à combler dans une implantation.
117 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
118 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
121 objects
= PosteManager()
124 nom
= models
.CharField(max_length
=255,
125 verbose_name
= u
"Titre du poste", )
126 nom_feminin
= models
.CharField(max_length
=255,
127 verbose_name
= u
"Titre du poste (au féminin)",
129 implantation
= models
.ForeignKey(ref
.Implantation
, help_text
=u
"Taper le nom de l'implantation ou sa région",
130 db_column
='implantation', related_name
='+')
131 type_poste
= models
.ForeignKey('TypePoste', db_column
='type_poste', help_text
=u
"Taper le nom du type de poste",
134 verbose_name
=u
"type de poste")
135 service
= models
.ForeignKey('Service', db_column
='service',
137 verbose_name
= u
"direction/service/pôle support",
139 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
142 help_text
=u
"Taper le nom du poste ou du type de poste",
143 verbose_name
= u
"Poste du responsable", )
146 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
148 verbose_name
= u
"Temps de travail",
149 help_text
="% du temps complet")
150 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
151 decimal_places
=2, null
=True,
152 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
153 verbose_name
= u
"Nb. heures par semaine",
154 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
)
157 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
158 null
=True, blank
=True)
159 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
160 null
=True, blank
=True)
161 mise_a_disposition
= models
.NullBooleanField(
162 verbose_name
= u
"Mise à disposition",
163 null
=True, default
=False)
164 appel
= models
.CharField(max_length
=10, null
=True,
165 verbose_name
= u
"Appel à candidature",
166 choices
=POSTE_APPEL_CHOICES
,
170 classement_min
= models
.ForeignKey('Classement',
171 db_column
='classement_min', related_name
='+',
172 null
=True, blank
=True)
173 classement_max
= models
.ForeignKey('Classement',
174 db_column
='classement_max', related_name
='+',
175 null
=True, blank
=True)
176 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
177 db_column
='valeur_point_min', related_name
='+',
178 null
=True, blank
=True)
179 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
180 db_column
='valeur_point_max', related_name
='+',
181 null
=True, blank
=True)
182 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
184 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
186 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
187 null
=True, default
=0)
188 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
189 null
=True, default
=0)
190 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
191 null
=True, default
=0)
192 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
193 null
=True, default
=0)
194 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
195 null
=True, default
=0)
196 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
197 null
=True, default
=0)
199 # Comparatifs de rémunération
200 devise_comparaison
= models
.ForeignKey('Devise', null
=True, blank
=True,
201 db_column
='devise_comparaison',
203 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
204 null
=True, blank
=True)
205 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
206 null
=True, blank
=True)
207 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
208 null
=True, blank
=True)
209 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
210 null
=True, blank
=True)
211 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
212 null
=True, blank
=True)
213 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
214 null
=True, blank
=True)
215 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
216 null
=True, blank
=True)
217 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
218 null
=True, blank
=True)
219 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
220 null
=True, blank
=True)
221 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
222 null
=True, blank
=True)
225 justification
= models
.TextField(null
=True, blank
=True)
228 date_debut
= models
.DateField(verbose_name
=u
"Date de début", help_text
=HELP_TEXT_DATE
,
229 null
=True, blank
=True)
230 date_fin
= models
.DateField(verbose_name
=u
"Date de fin", help_text
=HELP_TEXT_DATE
,
231 null
=True, blank
=True)
235 ordering
= ['implantation__nom', 'nom']
236 verbose_name
= u
"Poste"
237 verbose_name_plural
= u
"Postes"
240 def __unicode__(self
):
241 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
243 return representation
246 prefix_implantation
= "implantation__region"
247 def get_regions(self
):
248 return [self
.implantation
.region
]
250 def get_devise(self
):
251 return ValeurPoint
.objects
.filter(implantation
=self
.implantation
).order_by('annee')[0].devise
254 __doc__
= Poste_
.__doc__
256 # meta dématérialisation : pour permettre le filtrage
257 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
261 if self
.occupe_par():
265 def occupe_par(self
):
266 """Retourne la liste d'employé occupant ce poste.
267 Généralement, retourne une liste d'un élément.
268 Si poste inoccupé, retourne liste vide.
269 UTILISE pour mettre a jour le flag vacant
271 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
274 POSTE_FINANCEMENT_CHOICES
= (
275 ('A', 'A - Frais de personnel'),
276 ('B', 'B - Projet(s)-Titre(s)'),
281 class PosteFinancement_(models
.Model
):
282 """Pour un Poste, structure d'informations décrivant comment on prévoit
285 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
286 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
287 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
288 help_text
="ex.: 33.33 % (décimale avec point)")
289 commentaire
= models
.TextField(
290 help_text
="Spécifiez la source de financement.")
296 def __unicode__(self
):
297 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
300 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
303 class PosteFinancement(PosteFinancement_
):
307 class PostePiece_(models
.Model
):
308 """Documents relatifs au Poste.
309 Ex.: Description de poste
311 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
312 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
313 fichier
= models
.FileField(verbose_name
= u
"Fichier",
314 upload_to
=poste_piece_dispatch
,
315 storage
=storage_prive
)
321 def __unicode__(self
):
322 return u
'%s' % (self
.nom
)
324 class PostePiece(PostePiece_
):
327 class PosteComparaison_(AUFMetadata
, DevisableMixin
):
329 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
331 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
332 objects
= PosteComparaisonManager()
334 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
335 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
336 montant
= models
.IntegerField(null
=True)
337 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
343 def __unicode__(self
):
346 class PosteComparaison(PosteComparaison_
):
347 objects
= NoDeleteManager()
349 class PosteCommentaire_(Commentaire
):
350 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
355 class PosteCommentaire(PosteCommentaire_
):
360 class Employe(AUFMetadata
):
361 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
362 Dossiers qu'il occupe ou a occupé de Postes.
364 Cette classe aurait pu avantageusement s'appeler Personne car la notion
365 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
368 nom
= models
.CharField(max_length
=255)
369 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
370 nom_affichage
= models
.CharField(max_length
=255,
371 verbose_name
= u
"Nom d'affichage",
372 null
=True, blank
=True)
373 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
374 db_column
='nationalite',
375 related_name
='employes_nationalite',
376 verbose_name
= u
"Nationalité",
377 blank
=True, null
=True)
378 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
379 help_text
=HELP_TEXT_DATE
,
380 validators
=[validate_date_passee
],
381 null
=True, blank
=True)
382 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
385 situation_famille
= models
.CharField(max_length
=1,
386 choices
=SITUATION_CHOICES
,
387 verbose_name
= u
"Situation familiale",
388 null
=True, blank
=True)
389 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
390 help_text
=HELP_TEXT_DATE
,
391 null
=True, blank
=True)
394 tel_domicile
= models
.CharField(max_length
=255,
395 verbose_name
= u
"Tél. domicile",
396 null
=True, blank
=True)
397 tel_cellulaire
= models
.CharField(max_length
=255,
398 verbose_name
= u
"Tél. cellulaire",
399 null
=True, blank
=True)
400 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
401 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
402 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
403 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
404 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
405 related_name
='employes',
406 null
=True, blank
=True)
408 # meta dématérialisation : pour permettre le filtrage
409 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
412 ordering
= ['nom','prenom']
413 verbose_name
= u
"Employé"
414 verbose_name_plural
= u
"Employés"
416 def __unicode__(self
):
417 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
421 if self
.genre
.upper() == u
'M':
423 elif self
.genre
.upper() == u
'F':
428 """Retourne l'URL du service retournant la photo de l'Employe.
429 Équivalent reverse url 'rh_photo' avec id en param.
431 from django
.core
.urlresolvers
import reverse
432 return reverse('rh_photo', kwargs
={'id':self
.id})
434 def dossiers_passes(self
):
435 params
= {KEY_STATUT
: STATUT_INACTIF
, }
436 search
= RechercheTemporelle(params
, self
.__class__
)
437 search
.purge_params(params
)
438 q
= search
.get_q_temporel(self
.rh_dossiers
)
439 return self
.rh_dossiers
.filter(q
)
441 def dossiers_futurs(self
):
442 params
= {KEY_STATUT
: STATUT_FUTUR
, }
443 search
= RechercheTemporelle(params
, self
.__class__
)
444 search
.purge_params(params
)
445 q
= search
.get_q_temporel(self
.rh_dossiers
)
446 return self
.rh_dossiers
.filter(q
)
448 def dossiers_encours(self
):
449 params
= {KEY_STATUT
: STATUT_ACTIF
, }
450 search
= RechercheTemporelle(params
, self
.__class__
)
451 search
.purge_params(params
)
452 q
= search
.get_q_temporel(self
.rh_dossiers
)
453 return self
.rh_dossiers
.filter(q
)
455 def postes_encours(self
):
456 postes_encours
= set()
457 for d
in self
.dossiers_encours():
458 postes_encours
.add(d
.poste
)
459 return postes_encours
461 def poste_principal(self
):
463 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
465 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
467 poste
= Poste
.objects
.none()
469 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
474 prefix_implantation
= "rh_dossiers__poste__implantation__region"
475 def get_regions(self
):
477 for d
in self
.dossiers
.all():
478 regions
.append(d
.poste
.implantation
.region
)
482 class EmployePiece(models
.Model
):
483 """Documents relatifs à un employé.
486 employe
= models
.ForeignKey('Employe', db_column
='employe')
487 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
488 fichier
= models
.FileField(verbose_name
="Fichier",
489 upload_to
=employe_piece_dispatch
,
490 storage
=storage_prive
)
494 verbose_name
= u
"Employé pièce"
495 verbose_name_plural
= u
"Employé pièces"
497 def __unicode__(self
):
498 return u
'%s' % (self
.nom
)
500 class EmployeCommentaire(Commentaire
):
501 employe
= models
.ForeignKey('Employe', db_column
='employe',
505 verbose_name
= u
"Employé commentaire"
506 verbose_name_plural
= u
"Employé commentaires"
509 LIEN_PARENTE_CHOICES
= (
510 ('Conjoint', 'Conjoint'),
511 ('Conjointe', 'Conjointe'),
516 class AyantDroit(AUFMetadata
):
517 """Personne en relation avec un Employe.
520 nom
= models
.CharField(max_length
=255)
521 prenom
= models
.CharField(max_length
=255,
522 verbose_name
= u
"Prénom",)
523 nom_affichage
= models
.CharField(max_length
=255,
524 verbose_name
= u
"Nom d'affichage",
525 null
=True, blank
=True)
526 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
527 db_column
='nationalite',
528 related_name
='ayantdroits_nationalite',
529 verbose_name
= u
"Nationalité",
530 null
=True, blank
=True)
531 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
532 help_text
=HELP_TEXT_DATE
,
533 validators
=[validate_date_passee
],
534 null
=True, blank
=True)
535 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
538 employe
= models
.ForeignKey('Employe', db_column
='employe',
539 related_name
='ayantdroits',
540 verbose_name
= u
"Employé")
541 lien_parente
= models
.CharField(max_length
=10,
542 choices
=LIEN_PARENTE_CHOICES
,
543 verbose_name
= u
"Lien de parenté",
544 null
=True, blank
=True)
548 verbose_name
= u
"Ayant droit"
549 verbose_name_plural
= u
"Ayants droit"
551 def __unicode__(self
):
552 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
554 prefix_implantation
= "employe__dossiers__poste__implantation__region"
555 def get_regions(self
):
557 for d
in self
.employe
.dossiers
.all():
558 regions
.append(d
.poste
.implantation
.region
)
562 class AyantDroitCommentaire(Commentaire
):
563 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
569 STATUT_RESIDENCE_CHOICES
= (
571 ('expat', 'Expatrié'),
574 COMPTE_COMPTA_CHOICES
= (
580 class Dossier_(AUFMetadata
, DevisableMixin
):
581 """Le Dossier regroupe les informations relatives à l'occupation
582 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
585 Plusieurs Contrats peuvent être associés au Dossier.
586 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
587 lequel aucun Dossier n'existe est un poste vacant.
590 objects
= DossierManager()
593 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
594 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
595 db_column
='organisme_bstg',
597 verbose_name
= u
"Organisme",
598 help_text
="Si détaché (DET) ou \
599 mis à disposition (MAD), \
600 préciser l'organisme.",
601 null
=True, blank
=True)
604 remplacement
= models
.BooleanField(default
=False)
605 remplacement_de
= models
.ForeignKey('self', related_name
='+',
606 help_text
=u
"Taper le nom de l'employé",
607 null
=True, blank
=True)
608 statut_residence
= models
.CharField(max_length
=10, default
='local',
609 verbose_name
= u
"Statut", null
=True,
610 choices
=STATUT_RESIDENCE_CHOICES
)
613 classement
= models
.ForeignKey('Classement', db_column
='classement',
615 null
=True, blank
=True)
616 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
618 default
=REGIME_TRAVAIL_DEFAULT
,
619 verbose_name
= u
"Régime de travail",
620 help_text
="% du temps complet")
621 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
622 decimal_places
=2, null
=True,
623 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
624 verbose_name
=u
"Nb. heures par semaine",
625 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
)
627 # Occupation du Poste par cet Employe (anciennement "mandat")
628 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
630 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
632 null
=True, blank
=True)
639 ordering
= ['employe__nom', ]
640 verbose_name
= u
"Dossier"
641 verbose_name_plural
= "Dossiers"
643 def salaire_theorique(self
):
644 annee
= date
.today().year
645 coeff
= self
.classement
.coefficient
646 implantation
= self
.poste
.implantation
647 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
649 montant
= coeff
* point
.valeur
650 devise
= point
.devise
651 return {'montant':montant
, 'devise':devise
}
653 def __unicode__(self
):
654 poste
= self
.poste
.nom
655 if self
.employe
.genre
== 'F':
656 poste
= self
.poste
.nom_feminin
657 return u
'%s - %s' % (self
.employe
, poste
)
659 prefix_implantation
= "poste__implantation__region"
660 def get_regions(self
):
661 return [self
.poste
.implantation
.region
]
664 def remunerations(self
):
665 key
= "%s_remunerations" % self
._meta
.app_label
666 remunerations
= getattr(self
, key
)
667 return remunerations
.all().order_by('-date_debut')
669 def remunerations_en_cours(self
):
670 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
671 return self
.remunerations().all().filter(q
).order_by('date_debut')
673 def get_salaire(self
):
675 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
679 def get_salaire_euros(self
):
680 tx
= self
.taux_devise()
681 return (float)(tx
) * (float)(self
.salaire
)
683 def get_remunerations_brutes(self
):
687 4 Indemnité d'expatriation
688 5 Indemnité pour frais
689 6 Indemnité de logement
690 7 Indemnité de fonction
691 8 Indemnité de responsabilité
692 9 Indemnité de transport
693 10 Indemnité compensatrice
694 11 Indemnité de subsistance
695 12 Indemnité différentielle
696 13 Prime d'installation
699 16 Indemnité de départ
700 18 Prime de 13ième mois
703 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
704 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
706 def get_charges_salariales(self
):
708 20 Charges salariales ?
711 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
713 def get_charges_patronales(self
):
715 17 Charges patronales
718 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
720 def get_remunerations_tierces(self
):
724 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in (2, )]
728 def get_total_local_charges_salariales(self
):
730 for r
in self
.get_charges_salariales():
734 def get_total_local_charges_patronales(self
):
736 for r
in self
.get_charges_patronales():
737 total
+= float(r
.montant
)
740 def get_local_salaire_brut(self
):
742 somme des rémuérations brutes
744 devise
= self
.poste
.get_devise()
746 for r
in self
.get_remunerations_brutes():
747 if r
.devise
!= devise
:
749 total
+= float(r
.montant
)
752 def get_local_salaire_net(self
):
754 salaire brut - charges salariales
756 devise
= self
.poste
.get_devise()
758 for r
in self
.get_charges_salariales():
759 if r
.devise
!= devise
:
761 total_charges
+= float(r
.montant
)
762 return self
.get_local_salaire_brut() - total_charges
764 def get_local_couts_auf(self
):
766 salaire net + charges patronales
768 devise
= self
.poste
.get_devise()
770 for r
in self
.get_charges_patronales():
771 if r
.devise
!= devise
:
773 total_charges
+= float(r
.montant
)
774 return self
.get_local_salaire_net() + total_charges
776 def get_total_local_remunerations_tierces(self
):
777 devise
= self
.poste
.get_devise()
779 for r
in self
.get_remunerations_tierces():
780 if r
.devise
!= devise
:
782 total
+= float(r
.montant
)
787 def get_total_charges_salariales(self
):
789 for r
in self
.get_charges_salariales():
790 total
+= r
.montant_euros()
793 def get_total_charges_patronales(self
):
795 for r
in self
.get_charges_patronales():
796 total
+= r
.montant_euros()
799 def get_salaire_brut(self
):
801 somme des rémuérations brutes
804 for r
in self
.get_remunerations_brutes():
805 total
+= r
.montant_euros()
808 def get_salaire_net(self
):
810 salaire brut - charges salariales
813 for r
in self
.get_charges_salariales():
814 total_charges
+= r
.montant_euros()
815 return self
.get_salaire_brut() - total_charges
817 def get_couts_auf(self
):
819 salaire net + charges patronales
822 for r
in self
.get_charges_patronales():
823 total_charges
+= r
.montant_euros()
824 return self
.get_salaire_net() + total_charges
826 def get_total_remunerations_tierces(self
):
828 for r
in self
.get_remunerations_tierces():
829 total
+= r
.montant_euros()
833 class Dossier(Dossier_
):
834 __doc__
= Dossier_
.__doc__
835 poste
= models
.ForeignKey('%s.Poste' % app_context(),
837 related_name
='%(app_label)s_dossiers',
838 help_text
=u
"Taper le nom du poste ou du type de poste",
840 employe
= models
.ForeignKey('Employe', db_column
='employe',
841 help_text
=u
"Taper le nom de l'employé",
842 related_name
='%(app_label)s_dossiers',
843 verbose_name
=u
"Employé")
844 principal
= models
.BooleanField(verbose_name
=u
"Principal?", default
=True,
845 help_text
=u
"Ce Dossier est pour le principal Poste occupé par l'Employé")
848 class DossierPiece_(models
.Model
):
849 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
850 Ex.: Lettre de motivation.
852 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_dossierpieces')
853 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
854 fichier
= models
.FileField(verbose_name
= u
"Fichier",
855 upload_to
=dossier_piece_dispatch
,
856 storage
=storage_prive
)
862 def __unicode__(self
):
863 return u
'%s' % (self
.nom
)
865 class DossierPiece(DossierPiece_
):
868 class DossierCommentaire_(Commentaire
):
869 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
873 class DossierCommentaire(DossierCommentaire_
):
876 class DossierComparaison_(models
.Model
, DevisableMixin
):
878 Photo d'une comparaison salariale au moment de l'embauche.
880 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
881 objects
= DossierComparaisonManager()
883 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
884 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
885 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
886 montant
= models
.IntegerField(null
=True)
887 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
892 def __unicode__(self
):
893 return "%s (%s)" % (self
.poste
, self
.personne
)
896 class DossierComparaison(DossierComparaison_
):
901 class RemunerationMixin(AUFMetadata
):
902 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
904 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
906 verbose_name
= u
"Type de rémunération")
907 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
908 db_column
='type_revalorisation',
910 verbose_name
= u
"Type de revalorisation",
911 null
=True, blank
=True)
912 montant
= models
.DecimalField(null
=True, blank
=True,
913 default
=0, max_digits
=12, decimal_places
=2)
914 # Annuel (12 mois, 52 semaines, 364 jours?)
915 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
916 # commentaire = precision
917 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
918 # date_debut = anciennement date_effectif
919 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
920 null
=True, blank
=True)
921 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
922 null
=True, blank
=True)
926 ordering
= ['type__nom', '-date_fin']
928 def __unicode__(self
):
929 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
931 class Remuneration_(RemunerationMixin
, DevisableMixin
):
932 """Structure de rémunération (données budgétaires) en situation normale
933 pour un Dossier. Si un Evenement existe, utiliser la structure de
934 rémunération EvenementRemuneration de cet événement.
937 def montant_mois(self
):
938 return round(self
.montant
/ 12, 2)
940 def montant_avec_regime(self
):
941 return round(self
.montant
* (self
.dossier
.regime_travail
/100), 2)
943 def montant_euro_mois(self
):
944 return round(self
.montant_euros() / 12, 2)
946 def __unicode__(self
):
948 devise
= self
.devise
.code
951 return "%s %s" % (self
.montant
, devise
)
955 verbose_name
= u
"Rémunération"
956 verbose_name_plural
= u
"Rémunérations"
959 class Remuneration(Remuneration_
):
965 class ContratManager(NoDeleteManager
):
966 def get_query_set(self
):
967 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
970 class Contrat_(AUFMetadata
):
971 """Document juridique qui encadre la relation de travail d'un Employe
972 pour un Poste particulier. Pour un Dossier (qui documente cette
973 relation de travail) plusieurs contrats peuvent être associés.
975 objects
= ContratManager()
976 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
977 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
979 verbose_name
= u
"type de contrat")
980 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
981 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
982 null
=True, blank
=True)
983 fichier
= models
.FileField(verbose_name
= u
"Fichier",
984 upload_to
=contrat_dispatch
,
985 storage
=storage_prive
,
986 null
=True, blank
=True)
990 ordering
= ['dossier__employe__nom']
991 verbose_name
= u
"Contrat"
992 verbose_name_plural
= u
"Contrats"
994 def __unicode__(self
):
995 return u
'%s - %s' % (self
.dossier
, self
.id)
997 class Contrat(Contrat_
):
1003 #class Evenement_(AUFMetadata):
1004 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
1005 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
1006 # (ex.: la Remuneration).
1008 # Ex.: congé de maternité, maladie...
1010 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
1011 # différent et une rémunération en conséquence. On souhaite toutefois
1012 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
1013 # du retour à la normale.
1015 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
1017 # nom = models.CharField(max_length=255)
1018 # date_debut = models.DateField(verbose_name = u"Date de début")
1019 # date_fin = models.DateField(verbose_name = u"Date de fin",
1020 # null=True, blank=True)
1024 # ordering = ['nom']
1025 # verbose_name = u"Évènement"
1026 # verbose_name_plural = u"Évènements"
1028 # def __unicode__(self):
1029 # return u'%s' % (self.nom)
1032 #class Evenement(Evenement_):
1033 # __doc__ = Evenement_.__doc__
1036 #class EvenementRemuneration_(RemunerationMixin):
1037 # """Structure de rémunération liée à un Evenement qui remplace
1038 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
1041 # evenement = models.ForeignKey("Evenement", db_column='evenement',
1043 # verbose_name = u"Évènement")
1044 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
1045 # # de l'Evenement associé
1049 # ordering = ['evenement', 'type__nom', '-date_fin']
1050 # verbose_name = u"Évènement - rémunération"
1051 # verbose_name_plural = u"Évènements - rémunérations"
1054 #class EvenementRemuneration(EvenementRemuneration_):
1055 # __doc__ = EvenementRemuneration_.__doc__
1061 #class EvenementRemuneration(EvenementRemuneration_):
1062 # __doc__ = EvenementRemuneration_.__doc__
1063 # TODO? class ContratPiece(models.Model):
1068 class FamilleEmploi(AUFMetadata
):
1069 """Catégorie utilisée dans la gestion des Postes.
1070 Catégorie supérieure à TypePoste.
1072 nom
= models
.CharField(max_length
=255)
1076 verbose_name
= u
"Famille d'emploi"
1077 verbose_name_plural
= u
"Familles d'emploi"
1079 def __unicode__(self
):
1080 return u
'%s' % (self
.nom
)
1082 class TypePoste(AUFMetadata
):
1083 """Catégorie de Poste.
1085 nom
= models
.CharField(max_length
=255)
1086 nom_feminin
= models
.CharField(max_length
=255,
1087 verbose_name
= u
"Nom féminin")
1089 is_responsable
= models
.BooleanField(default
=False,
1090 verbose_name
= u
"Poste de responsabilité")
1091 famille_emploi
= models
.ForeignKey('FamilleEmploi',
1092 db_column
='famille_emploi',
1094 verbose_name
= u
"famille d'emploi")
1098 verbose_name
= u
"Type de poste"
1099 verbose_name_plural
= u
"Types de poste"
1101 def __unicode__(self
):
1102 return u
'%s' % (self
.nom
)
1105 TYPE_PAIEMENT_CHOICES
= (
1106 (u
'Régulier', u
'Régulier'),
1107 (u
'Ponctuel', u
'Ponctuel'),
1110 NATURE_REMUNERATION_CHOICES
= (
1111 (u
'Accessoire', u
'Accessoire'),
1112 (u
'Charges', u
'Charges'),
1113 (u
'Indemnité', u
'Indemnité'),
1114 (u
'RAS', u
'Rémunération autre source'),
1115 (u
'Traitement', u
'Traitement'),
1118 class TypeRemuneration(AUFMetadata
):
1119 """Catégorie de Remuneration.
1121 objects
= TypeRemunerationManager()
1123 nom
= models
.CharField(max_length
=255)
1124 type_paiement
= models
.CharField(max_length
=30,
1125 choices
=TYPE_PAIEMENT_CHOICES
,
1126 verbose_name
= u
"Type de paiement")
1127 nature_remuneration
= models
.CharField(max_length
=30,
1128 choices
=NATURE_REMUNERATION_CHOICES
,
1129 verbose_name
= u
"Nature de la rémunération")
1130 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1134 verbose_name
= u
"Type de rémunération"
1135 verbose_name_plural
= u
"Types de rémunération"
1137 def __unicode__(self
):
1139 archive
= u
"(archivé)"
1142 return u
'%s %s' % (self
.nom
, archive
)
1144 class TypeRevalorisation(AUFMetadata
):
1145 """Justification du changement de la Remuneration.
1146 (Actuellement utilisé dans aucun traitement informatique.)
1148 nom
= models
.CharField(max_length
=255)
1152 verbose_name
= u
"Type de revalorisation"
1153 verbose_name_plural
= u
"Types de revalorisation"
1155 def __unicode__(self
):
1156 return u
'%s' % (self
.nom
)
1158 class Service(AUFMetadata
):
1159 """Unité administrative où les Postes sont rattachés.
1161 objects
= ServiceManager()
1163 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1164 nom
= models
.CharField(max_length
=255)
1168 verbose_name
= u
"Service"
1169 verbose_name_plural
= u
"Services"
1171 def __unicode__(self
):
1173 archive
= u
"(archivé)"
1176 return u
'%s %s' % (self
.nom
, archive
)
1179 TYPE_ORGANISME_CHOICES
= (
1180 ('MAD', 'Mise à disposition'),
1181 ('DET', 'Détachement'),
1184 class OrganismeBstg(AUFMetadata
):
1185 """Organisation d'où provient un Employe mis à disposition (MAD) de
1186 ou détaché (DET) à l'AUF à titre gratuit.
1188 (BSTG = bien et service à titre gratuit.)
1190 nom
= models
.CharField(max_length
=255)
1191 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1192 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1194 related_name
='organismes_bstg',
1195 null
=True, blank
=True)
1198 ordering
= ['type', 'nom']
1199 verbose_name
= u
"Organisme BSTG"
1200 verbose_name_plural
= u
"Organismes BSTG"
1202 def __unicode__(self
):
1203 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1205 prefix_implantation
= "pays__region"
1206 def get_regions(self
):
1207 return [self
.pays
.region
]
1210 class Statut(AUFMetadata
):
1211 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1214 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.")
1215 nom
= models
.CharField(max_length
=255)
1219 verbose_name
= u
"Statut d'employé"
1220 verbose_name_plural
= u
"Statuts d'employé"
1222 def __unicode__(self
):
1223 return u
'%s : %s' % (self
.code
, self
.nom
)
1226 TYPE_CLASSEMENT_CHOICES
= (
1227 ('S', 'S -Soutien'),
1228 ('T', 'T - Technicien'),
1229 ('P', 'P - Professionel'),
1231 ('D', 'D - Direction'),
1232 ('SO', 'SO - Sans objet [expatriés]'),
1233 ('HG', 'HG - Hors grille [direction]'),
1236 class ClassementManager(models
.Manager
):
1238 Ordonner les spcéfiquement les classements.
1240 def get_query_set(self
):
1241 qs
= super(self
.__class__
, self
).get_query_set()
1242 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1243 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1247 class Classement_(AUFMetadata
):
1248 """Éléments de classement de la
1249 "Grille générique de classement hiérarchique".
1251 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1252 classement dans la grille. Le classement donne le coefficient utilisé dans:
1254 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1256 objects
= ClassementManager()
1259 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1260 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1261 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1262 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1265 # annee # au lieu de date_debut et date_fin
1266 commentaire
= models
.TextField(null
=True, blank
=True)
1270 ordering
= ['type','echelon','degre','coefficient']
1271 verbose_name
= u
"Classement"
1272 verbose_name_plural
= u
"Classements"
1274 def __unicode__(self
):
1275 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1277 class Classement(Classement_
):
1278 __doc__
= Classement_
.__doc__
1281 class TauxChange_(AUFMetadata
):
1282 """Taux de change de la devise vers l'euro (EUR)
1283 pour chaque année budgétaire.
1286 devise
= models
.ForeignKey('Devise', db_column
='devise')
1287 annee
= models
.IntegerField(verbose_name
= u
"Année")
1288 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1292 ordering
= ['-annee', 'devise__code']
1293 verbose_name
= u
"Taux de change"
1294 verbose_name_plural
= u
"Taux de change"
1296 def __unicode__(self
):
1297 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1300 class TauxChange(TauxChange_
):
1301 __doc__
= TauxChange_
.__doc__
1303 class ValeurPointManager(NoDeleteManager
):
1305 def get_query_set(self
):
1306 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1309 class ValeurPoint_(AUFMetadata
):
1310 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1311 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1312 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1314 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1317 actuelles
= ValeurPointManager()
1319 valeur
= models
.FloatField(null
=True)
1320 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1321 implantation
= models
.ForeignKey(ref
.Implantation
,
1322 db_column
='implantation',
1323 related_name
='%(app_label)s_valeur_point')
1325 annee
= models
.IntegerField()
1328 ordering
= ['-annee', 'implantation__nom']
1330 verbose_name
= u
"Valeur du point"
1331 verbose_name_plural
= u
"Valeurs du point"
1333 def __unicode__(self
):
1334 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1337 class ValeurPoint(ValeurPoint_
):
1338 __doc__
= ValeurPoint_
.__doc__
1342 class Devise(AUFMetadata
):
1343 """Devise monétaire.
1346 objects
= DeviseManager()
1348 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1349 code
= models
.CharField(max_length
=10, unique
=True)
1350 nom
= models
.CharField(max_length
=255)
1354 verbose_name
= u
"Devise"
1355 verbose_name_plural
= u
"Devises"
1357 def __unicode__(self
):
1358 return u
'%s - %s' % (self
.code
, self
.nom
)
1360 class TypeContrat(AUFMetadata
):
1363 nom
= models
.CharField(max_length
=255)
1364 nom_long
= models
.CharField(max_length
=255)
1368 verbose_name
= u
"Type de contrat"
1369 verbose_name_plural
= u
"Types de contrat"
1371 def __unicode__(self
):
1372 return u
'%s' % (self
.nom
)
1377 class ResponsableImplantation(AUFMetadata
):
1378 """Le responsable d'une implantation.
1379 Anciennement géré sur le Dossier du responsable.
1381 employe
= models
.ForeignKey('Employe', db_column
='employe',
1383 null
=True, blank
=True)
1384 implantation
= models
.ForeignKey(ref
.Implantation
,
1385 db_column
='implantation', related_name
='+',
1388 def __unicode__(self
):
1389 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1392 ordering
= ['implantation__nom']
1393 verbose_name
= "Responsable d'implantation"
1394 verbose_name_plural
= "Responsables d'implantation"