from django.core.files.storage import FileSystemStorage
from django.db import models
import reversion
- from workflow import PosteWorkflow
+ from workflow import PosteWorkflow, DossierWorkflow
+ from managers import DossierManager, PosteManager
import datamaster_modeles.models as ref
from rh_v1 import models as rh
- import settings
-STATUT_RESIDENCE_CHOICES = (
- ('local', 'Local'),
- ('expat', 'Expatrié'),
-)
-POSTE_APPEL_CHOICES = (
- ('interne', 'Interne'),
- ('externe', 'Externe'),
-)
+# Constantes
+HELP_TEXT_DATE = "format: aaaa-mm-jj"
+REGIME_TRAVAIL_DEFAULT=100.00
+REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT=35.00
-POSTE_STATUT_CHOICES = (
- ('MAD', 'Mise à disposition'),
- ('DET', 'Détachement'),
-)
# Upload de fichiers
-storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT, base_url=settings.PRIVE_MEDIA_URL)
+storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
+ base_url=settings.PRIVE_MEDIA_URL)
def poste_piece_dispatch(instance, filename):
path = "poste/%s/%s" % (instance.poste_id, filename)
return path
-class PostePiece(models.Model):
- poste = models.ForeignKey("Poste")
- nom = models.CharField(verbose_name="Nom", max_length=255)
- fichier = models.FileField(verbose_name="Fichier", upload_to=poste_piece_dispatch, storage=storage_prive)
+### POSTE
+
+POSTE_APPEL_CHOICES = (
+ ('interne', 'Interne'),
+ ('externe', 'Externe'),
+)
- class PosteManager(models.Manager):
- """
- Chargement de tous les objets FK existants sur chaque QuerySet.
- """
- def get_query_set(self):
- fkeys = (
- 'id_rh',
- 'responsable',
- 'implantation',
- 'type_poste',
- 'service',
- 'classement_min',
- 'classement_max',
- 'valeur_point_min',
- 'valeur_point_max',
- )
- return super(PosteManager, self).get_query_set() \
- .select_related(*fkeys).all()
-
class Poste(PosteWorkflow, models.Model):
# Modèle existant
implantation_devise = 5 # EUR
return implantation_devise
+ #####################
+ # Classement de poste
+ #####################
+
+ def get_couts_minimum(self):
+ return (float)(self.salaire_min + self.indemn_min + self.autre_min)
+
+ def get_taux_minimum(self):
+ try:
+ return rh.TauxChange.objects.filter(implantation=self.implantation, devise=self.devise_min)[0].taux
+ except:
+ return 1
+
+ def get_couts_minimum_euros(self):
+ return self.get_couts_minimum() * self.get_taux_minimum()
+
+ def get_couts_maximum(self):
+ return (float)(self.salaire_max + self.indemn_max + self.autre_max)
+
+ def get_taux_maximum(self):
+ try:
+ return rh.TauxChange.objects.filter(implantation=self.implantation, devise=self.devise_max)[0].taux
+ except:
+ return 1
+
+ def get_couts_maximum_euros(self):
+ return self.get_couts_maximum() * self.get_taux_maximum()
+
+ ######################
+ # Comparaison de poste
+ ######################
+
+ def est_comparable(self):
+ """
+ Si on a au moins une valeur de saisie dans les comparaisons, alors le poste
+ est comparable.
+ """
+ if self.comp_universite_min is None and \
+ self.comp_fonctionpub_min is None and \
+ self.comp_locale_min is None and \
+ self.comp_ong_min is None and \
+ self.comp_autre_min is None and \
+ self.comp_universite_max is None and \
+ self.comp_fonctionpub_max is None and \
+ self.comp_locale_max is None and \
+ self.comp_ong_max is None and \
+ self.comp_autre_max is None:
+ return False
+ else:
+ return True
+
+
+ def get_taux_comparaison(self):
+ try:
+ return rh.TauxChange.objects.filter(implantation=self.implantation, devise=self.devise_comparaison)[0].taux
+ except:
+ return 1
+
+ def get_comp_universite_min_euros(self):
+ return (float)(self.comp_universite_min) * self.get_taux_comparaison()
+
+ def get_comp_fonctionpub_min_euros(self):
+ return (float)(self.comp_fonctionpub_min) * self.get_taux_comparaison()
+
+ def get_comp_locale_min_euros(self):
+ return (float)(self.comp_locale_min) * self.get_taux_comparaison()
+
+ def get_comp_ong_min_euros(self):
+ return (float)(self.comp_ong_min) * self.get_taux_comparaison()
+
+ def get_comp_autre_min_euros(self):
+ return (float)(self.comp_autre_min) * self.get_taux_comparaison()
+
+ def get_comp_universite_max_euros(self):
+ return (float)(self.comp_universite_max) * self.get_taux_comparaison()
+
+ def get_comp_fonctionpub_max_euros(self):
+ return (float)(self.comp_fonctionpub_max) * self.get_taux_comparaison()
+
+ def get_comp_locale_max_euros(self):
+ return (float)(self.comp_locale_max) * self.get_taux_comparaison()
+
+ def get_comp_ong_max_euros(self):
+ return (float)(self.comp_ong_max) * self.get_taux_comparaison()
+
+ def get_comp_autre_max_euros(self):
+ return (float)(self.comp_autre_max) * self.get_taux_comparaison()
+
+
def __unicode__(self):
"""
- Cette fonction est consommatrice SQL car elle cherche les dossiers qui ont été liés à celui-ci.
+ Cette fonction est consommatrice SQL car elle cherche les dossiers
+ qui ont été liés à celui-ci.
"""
complement_nom_poste = self.get_complement_nom()
if complement_nom_poste is None:
class Meta:
ordering = ['type']
+ def __unicode__(self):
+ return u"%s %s %s" % (self.get_type_display(), self.pourcentage, self.commentaire)
+
++
+class PostePiece(models.Model):
+ """Documents relatifs au Poste
+ Ex.: Description de poste
+ """
+ poste = models.ForeignKey("Poste")
+ nom = models.CharField(verbose_name="Nom", max_length=255)
+ fichier = models.FileField(verbose_name="Fichier",
+ upload_to=poste_piece_dispatch,
+ storage=storage_prive)
+
+### EMPLOYÉ/PERSONNE
+
+# TODO : migration pour m -> M, f -> F
++
GENRE_CHOICES = (
('m', 'Homme'),
('f', 'Femme'),
('aucun', 'Aucun'),
)
- class Dossier(models.Model):
-class DossierPiece(models.Model):
- dossier = models.ForeignKey("Dossier")
- nom = models.CharField(verbose_name="Nom", max_length=255)
- fichier = models.FileField(verbose_name="Fichier", upload_to=dossier_piece_dispatch, storage=storage_prive)
-
-
+ class Dossier(DossierWorkflow, models.Model):
# Modèle existant
employe = models.ForeignKey('Employe', related_name='+', editable=False)
if not reversion.is_registered(Dossier):
reversion.register(Dossier)
+class DossierPiece(models.Model):
+ """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
+ Ex.: Lettre de motivation.
+ """
+ dossier = models.ForeignKey("Dossier")
+ nom = models.CharField(verbose_name="Nom", max_length=255)
+ fichier = models.FileField(verbose_name="Fichier",
+ upload_to=dossier_piece_dispatch,
+ storage=storage_prive)
+
+
+ class DossierComparaison(models.Model):
+ """
+ Photo d'une comparaison salariale au moment de l'embauche.
+ """
+ dossier = models.ForeignKey('Dossier', related_name='comparaisons')
+ implantation = models.ForeignKey(ref.Implantation, null=True, blank=True)
+ poste = models.CharField(max_length=255, null=True, blank=True)
+ personne = models.CharField(max_length=255, null=True, blank=True)
+ montant = models.IntegerField(null=True)
+ devise = models.ForeignKey(rh.Devise, default=5, related_name='+', null=True, blank=True)
+ montant_euros = models.IntegerField(null=True)
+
++
+### RÉMUNÉRATION
+
class Remuneration(models.Model):
# Identification
dossier = models.ForeignKey('Dossier', db_column='dossier')
('Fils', 'Fils'),
)
-class AyantDroit(models.Model):
- #Identification
- id = models.IntegerField(primary_key=True)
+class AyantDroit(Metadata):
+ """Personne en relation avec un Employe.
+ """
+ # Identification
nom = models.CharField(max_length=255)
prenom = models.CharField(max_length=255)
- #Relation
- employe = models.ForeignKey('Employe', db_column='employe', related_name='ayants_droit')
- lien_parente = models.CharField(max_length=10, choices=LIEN_PARENTE_CHOICES, null=True, blank=True)
- #Méta
- commentaire = models.TextField(null=True, blank=True)
- actif = models.BooleanField()
+ # TODO : nom_affichage doit être obligatoire, pas nom et prenom
+ nom_affichage = models.CharField(max_length=255,
+ verbose_name=u"Nom d'affichage",
+ null=True, blank=True)
+ nationalite = models.ForeignKey(ref.Pays, to_field='code',
+ db_column='nationalite',
+ related_name='ayantdroits_nationalite')
+ date_naissance = models.DateField(help_text=HELP_TEXT_DATE,
+ null=True, blank=True)
+ genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
+
+ # Relation
+ employe = models.ForeignKey('Employe', db_column='employe',
+ related_name='ayantdroits')
+ lien_parente = models.CharField(max_length=10,
+ choices=LIEN_PARENTE_CHOICES,
+ null=True, blank=True)
+
+ class Meta:
+ ordering = ['nom_affichage']
+ def __unicode__(self):
+ # TODO : gérer nom d'affichage
+ return u'%s %s' % (self.prenom, self.nom.upper())
+
+class AyantDroitCommentaire(Commentaire):
+ ayant_droit = models.ForeignKey("AyantDroit", db_column='ayant_droit',
+ related_name='+')
+
+
+### DOSSIER
+
+STATUT_RESIDENCE_CHOICES = (
+ ('local', 'Local'),
+ ('expat', 'Expatrié'),
+)
+
+COMPTE_COMPTA_CHOICES = (
+ ('coda', 'CODA'),
+ ('scs', 'SCS'),
+ ('aucun', 'Aucun'),
+)
+
+class Dossier(Metadata):
+ """Le Dossier regroupe les informations relatives à l'occupation
+ d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
+ par un Employe.
+
+ Plusieurs Contrats peuvent être associés au Dossier.
+ Une structure de Remuneration est rattachée au Dossier. Un Poste pour
+ lequel aucun Dossier n'existe est un poste vacant.
+ """
+ # Identification
+ employe = models.ForeignKey('Employe', db_column='employe',
+ related_name='+')
+ poste = models.ForeignKey('Poste', db_column='poste',
+ related_name='+', editable=False)
+ statut = models.ForeignKey('Statut', related_name='+', default=3)
+ organisme_bstg = models.ForeignKey('OrganismeBstg',
+ db_column='organisme_bstg',
+ related_name='+',
+ verbose_name=u"Organisme",
+ help_text=u"Si détaché (DET) ou \
+ mis à disposition (MAD), \
+ préciser l'organisme.",
+ null=True, blank=True)
+
+ # Recrutement
+ remplacement = models.BooleanField(default=False)
+ statut_residence = models.CharField(max_length=10, default='local',
+ verbose_name=u"Statut",
+ choices=STATUT_RESIDENCE_CHOICES)
+
+ # Rémunération
+ classement = models.ForeignKey('Classement', db_column='classement',
+ related_name='+',
+ null=True, blank=True)
+ regime_travail = models.DecimalField(max_digits=12,
+ decimal_places=2,
+ default=REGIME_TRAVAIL_DEFAULT,
+ verbose_name=u"Régime de travail",
+ help_text=u"% du temps complet")
+ regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
+ decimal_places=2,
+ default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
+ verbose_name=u"Nb. heures par semaine")
+
+ # Occupation du Poste par cet Employe (anciennement "mandat")
+ date_debut = models.DateField(verbose_name=u"Date de début d'occupation \
+ de poste",
+ help_text=HELP_TEXT_DATE)
+ date_fin = models.DateField(verbose_name=u"Date de fin d'occupation \
+ de poste",
+ help_text=HELP_TEXT_DATE,
+ null=True, blank=True)
+ # Comptes
+ # TODO?
+
+ class Meta:
+ ordering = ['poste__nom', 'employe__nom_affichage']
+
+ def __unicode__(self):
+ return u'%s - %s' % (self.poste.nom, self.employe)
+
+class DossierPiece(models.Model):
+ """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
+ Ex.: Lettre de motivation.
+ """
+ dossier = models.ForeignKey("Dossier", db_column='dossier',
+ related_name='+')
+ nom = models.CharField(verbose_name=u"Nom", max_length=255)
+ fichier = models.FileField(verbose_name=u"Fichier",
+ upload_to=dossier_piece_dispatch,
+ storage=storage_prive)
+
+ class Meta:
+ ordering = ['nom']
+
+ def __unicode__(self):
+ return u'%s' % (self.nom)
+
+class DossierCommentaire(Commentaire):
+ dossier = models.ForeignKey("Dossier", db_column='dossier',
+ related_name='+')
+
+
+### RÉMUNÉRATION
-class Remuneration(models.Model):
- #Identification
- id = models.IntegerField(primary_key=True)
+class RemunerationMixin(Metadata):
+ # Identification
dossier = models.ForeignKey('Dossier', db_column='dossier')
- type = models.ForeignKey('TypeRemuneration', db_column='type')
- type_revalorisation = models.ForeignKey('TypeRevalorisation', db_column='type_revalorisation')
- montant = models.FloatField()
- devise = models.ForeignKey('Devise', to_field='code', db_column='devise')
- date_effective = models.DateField()
- pourcentage = models.IntegerField()
- #Méta
- date_creation = models.DateField(auto_now_add=True)
- user_creation = models.IntegerField() #User ou employé
- desactivation = models.BooleanField() #
- date_desactivation = models.DateField()
- user_desactivation = models.IntegerField() #User ou employé
- annule = models.BooleanField()
- date_annule = models.DateField()
- user_annule = models.IntegerField() #User ou employé
+ type = models.ForeignKey('TypeRemuneration', db_column='type',
+ related_name='+')
+ type_revalorisation = models.ForeignKey('TypeRevalorisation',
+ db_column='type_revalorisation',
+ related_name='+',
+ null=True, blank=True)
+ montant = models.FloatField(null=True, blank=True,
+ default=0)
+ # Annuel (12 mois, 52 semaines, 364 jours?)
- devise = models.ForeignKey('Devise', to_field='code',
++ devise = models.ForeignKey('Devise', to_field='id',
+ db_column='devise', related_name='+',
+ default=5)
+ # commentaire = precision
+ commentaire = models.CharField(max_length=255, null=True, blank=True)
+ # date_debut = anciennement date_effectif
+ date_debut = models.DateField(help_text=HELP_TEXT_DATE,
+ null=True, blank=True)
+ date_fin = models.DateField(help_text=HELP_TEXT_DATE,
+ null=True, blank=True)
-class FamilleEmploi(models.Model):
- #Identification
- id = models.IntegerField(primary_key=True)
+ class Meta:
+ abstract = True
+ ordering = ['type__nom', '-date_fin']
+
+ def __unicode__(self):
+ return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
+
+class Remuneration(RemunerationMixin):
+ """Structure de rémunération (données budgétaires) en situation normale
+ pour un Dossier. Si un Evenement existe, utiliser la structure de
+ rémunération EvenementRemuneration de cet événement.
+ """
+
+ def montant_mois(self):
+ return round(self.montant / 12, 2)
+
+ def taux_devise(self):
+ return self.devise.tauxchange_set.order_by('-annee').all()[0].taux
+
+ def montant_euro(self):
+ return round(float(self.montant) / float(self.taux_devise()), 2)
+
+ def montant_euro_mois(self):
+ return round(self.montant_euro() / 12, 2)
+
+ def __unicode__(self):
+ try:
+ devise = self.devise.code
+ except:
+ devise = "???"
+ return "%s %s" % (self.montant, devise)
+
+
+### CONTRATS
+
+class Contrat(Metadata):
+ """Document juridique qui encadre la relation de travail d'un Employe
+ pour un Poste particulier. Pour un Dossier (qui documente cette
+ relation de travail) plusieurs contrats peuvent être associés.
+ """
+ dossier = models.ForeignKey('Dossier', db_column='dossier',
+ related_name='+')
+ type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat',
+ related_name='+')
+ date_debut = models.DateField(help_text=HELP_TEXT_DATE)
+ date_fin = models.DateField(help_text=HELP_TEXT_DATE,
+ null=True, blank=True)
+
+ class Meta:
+ ordering = ['dossier__employe__nom_affichage']
+
+ def __unicode__(self):
+ return u'%s - %s' % (self.dossier.employe.nom_affichage, self.id)
+
+# TODO? class ContratPiece(models.Model):
+
+
+### ÉVÉNEMENTS
+
+class Evenement(Metadata):
+ """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
+ d'un Dossier qui vient altérer des informations normales liées à un Dossier
+ (ex.: la Remuneration).
+
+ Ex.: congé de maternité, maladie...
+
+ Lors de ces situations exceptionnelles, l'Employe a un régime de travail
+ différent et une rémunération en conséquence. On souhaite toutefois
+ conserver le Dossier intact afin d'éviter une re-saisie des données lors
+ du retour à la normale.
+ """
+ dossier = models.ForeignKey("Dossier", db_column='dossier',
+ related_name='+')
+ nom = models.CharField(max_length=255)
+ date_debut = models.DateField(help_text=HELP_TEXT_DATE)
+ date_fin = models.DateField(help_text=HELP_TEXT_DATE,
+ null=True, blank=True)
+ class Meta:
+ ordering = ['nom']
+
+ def __unicode__(self):
+ return u'%s' % (self.nom)
+
+class EvenementRemuneration(RemunerationMixin):
+ """Structure de rémunération liée à un Evenement qui remplace
+ temporairement la Remuneration normale d'un Dossier, pour toute la durée
+ de l'Evenement.
+ """
+ evenement = models.ForeignKey("Evenement", db_column='evenement',
+ related_name='+')
+
+
+### RÉFÉRENCES RH
+
+class FamilleEmploi(Metadata):
+ """Catégorie utilisée dans la gestion des Postes.
+ Catégorie supérieure à TypePoste.
+ """
nom = models.CharField(max_length=255)
- #Méta
- actif = models.BooleanField()
+
+ def __unicode__(self):
+ return u'%s' % (self.nom)
-class TypePoste(models.Model):
- #Identification
- id = models.IntegerField(primary_key=True)
+class TypePoste(Metadata):
+ """Catégorie de Poste.
+ """
nom = models.CharField(max_length=255)
nom_feminin = models.CharField(max_length=255)
- description = models.CharField(max_length=255)
- is_responsable = models.BooleanField()
- famille_emploi = models.ForeignKey('FamilleEmploi', db_column='famille_emploi')
- #Méta
- date_modification = models.DateField(auto_now=True)
- actif = models.BooleanField()
+
+ is_responsable = models.BooleanField(default=False)
+ famille_emploi = models.ForeignKey('FamilleEmploi',
+ db_column='famille_emploi',
+ related_name='+')
+ class Meta:
+ ordering = ['nom']
+
def __unicode__(self):
- return u'%s' % self.nom
+ # TODO : gérer nom féminin
+ return u'%s' % (self.nom)
TYPE_PAIEMENT_CHOICES = (
id = models.IntegerField(primary_key=True)
dossier = models.ForeignKey('Dossier', db_column='dossier')
type = models.ForeignKey('TypeRemuneration', db_column='type')
- type_revalorisation = models.ForeignKey('TypeRevalorisation', db_column='type_revalorisation', null=True, blank=True)
+ type_revalorisation = models.ForeignKey('TypeRevalorisation',
+ db_column='type_revalorisation',
+ null=True, blank=True)
montant = models.FloatField(null=True, blank=True)
- devise = models.ForeignKey('Devise', to_field='code', db_column='devise',
- null=True, blank=True)
+ devise = models.ForeignKey('Devise', to_field='id', db_column='devise', null=True, blank=True)
date_effective = models.DateField(null=True, blank=True)
pourcentage = models.IntegerField(null=True, blank=True)
- #Méta
+ # Méta
date_creation = models.DateField(auto_now_add=True)
user_creation = models.IntegerField(null=True, blank=True) #User ou employé
desactivation = models.NullBooleanField(null=True, blank=True) #
devise = self.devise.code
except:
devise = "???"
- return "%s %s" % (self.montant, devise)
+ return "%s %s (%s EUR - %s)" % (self.montant, devise, self.en_euros(), self.get_taux_historique(), )
+
+ def get_taux_historique(self):
+ tauxchange = TauxChange.objects.filter(devise=self.devise, annee=self.date_creation.year)[0]
+ return tauxchange
+
+ def en_euros(self):
+ return int(self.montant * self.get_taux_historique().taux)
class FamilleEmploi(models.Model):
- #Identification
+ # Identification
id = models.IntegerField(primary_key=True)
nom = models.CharField(max_length=255)
- #Méta
+ # Méta
actif = models.BooleanField()
class TypePoste(models.Model):
ordering = ['type','echelon','degre','coefficient']
class TauxChange(models.Model):
- #Identification
+ # Identification
id = models.IntegerField(primary_key=True)
- devise = models.ForeignKey('Devise', db_column='devise')
+ devise = models.ForeignKey('Devise', to_field='code', db_column='devise')
annee = models.IntegerField()
taux = models.FloatField()
- #Relations
- implantation = models.ForeignKey('datamaster_modeles.Implantation', db_column='implantation')
+ # Relations
+ implantation = models.ForeignKey('datamaster_modeles.Implantation',
+ db_column='implantation',
+ related_name='taux_change')
+ def __unicode__(self):
+ return u"%s %s : %s" % (self.devise, self.annee, self.taux)
+
class ValeurPointManager(models.Manager):
"""
Manager qui travaille uniquement sur les valeurs du point de l'année en cours.