from datetime import date
from decimal import Decimal
+import reversion
+from auf.django.emploi.models import \
+ GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references
+from auf.django.references import models as ref
+from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
+from django.core.exceptions import MultipleObjectsReturned
from django.db import models
from django.db.models import Q
+from django.db.models.signals import post_save, pre_save
from django.conf import settings
-from auf.django.emploi.models import \
- GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references
-from auf.django.metadata.models import AUFMetadata
-from auf.django.metadata.managers import NoDeleteManager
-from auf.django.references import models as ref
-
from project.rh.change_list import \
RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \
STATUT_FUTUR
-from project.rh.managers import \
- PosteManager, DossierManager, DossierComparaisonManager, \
- PosteComparaisonManager, DeviseManager, ServiceManager, \
- TypeRemunerationManager
+from project import groups
+from project.rh.managers import (
+ PosteManager,
+ DossierManager,
+ EmployeManager,
+ DossierComparaisonManager,
+ PosteComparaisonManager,
+ ContratManager,
+ RemunerationManager,
+ ArchivableManager,
+ )
+
+
+TWOPLACES = Decimal('0.01')
+
from project.rh.validators import validate_date_passee
+# import pour relocaliser le modèle selon la convention (models.py pour
+# introspection)
+from project.rh.historique import ModificationTraite
# Constantes
HELP_TEXT_DATE = "format: jj-mm-aaaa"
base_url=settings.PRIVE_MEDIA_URL)
+class RemunIntegrityException(Exception):
+ pass
+
def poste_piece_dispatch(instance, filename):
path = "%s/poste/%s/%s" % (
instance._meta.app_label, instance.poste_id, filename
return path
+class DateActiviteMixin(models.Model):
+ """
+ Mixin pour mettre à jour l'activité d'un modèle
+ """
+ class Meta:
+ abstract = True
+ date_creation = models.DateTimeField(auto_now_add=True,
+ null=True, blank=True,
+ verbose_name=u"Date de création",)
+ date_modification = models.DateTimeField(auto_now=True,
+ null=True, blank=True,
+ verbose_name=u"Date de modification",)
+
+
+class Archivable(models.Model):
+ archive = models.BooleanField(u'archivé', default=False)
+
+ objects = ArchivableManager()
+ avec_archives = models.Manager()
+
+ class Meta:
+ abstract = True
+
+
class DevisableMixin(object):
def get_annee_pour_taux_devise(self):
return 1
annee = self.get_annee_pour_taux_devise()
- taux = [
- tc.taux
- for tc in TauxChange.objects.filter(devise=devise, annee=annee)
- ]
- taux = set(taux)
-
- if len(taux) == 0:
- raise Exception(
- u"Pas de taux pour %s en %s" % (devise.code, annee)
- )
+ taux = TauxChange.objects.filter(devise=devise, annee__lte=annee) \
+ .order_by('-annee')
+ return taux[0].taux
- if len(taux) > 1:
- raise Exception(u"Il existe plusieurs taux de %s en %s" %
- (devise.code, annee))
- else:
- return list(taux)[0]
-
- def montant_euros(self):
+ def montant_euros_float(self):
try:
taux = self.taux_devise()
except Exception, e:
return e
if not taux:
return None
- return int(round(float(self.montant) * float(taux), 2))
+ return float(self.montant) * float(taux)
+
+ def montant_euros(self):
+ return int(round(self.montant_euros_float(), 2))
-class Commentaire(AUFMetadata):
+class Commentaire(models.Model):
texte = models.TextField()
owner = models.ForeignKey(
'auth.User', db_column='owner', related_name='+',
verbose_name=u"Commentaire de"
)
+ date_creation = models.DateTimeField(
+ u'date', auto_now_add=True, blank=True, null=True
+ )
class Meta:
abstract = True
)
-class Poste_(AUFMetadata):
+class Poste_( DateActiviteMixin, models.Model,):
"""
Un Poste est un emploi (job) à combler dans une implantation.
Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
'Devise', db_column='devise_max', null=True, related_name='+'
)
salaire_min = models.DecimalField(
- max_digits=12, decimal_places=2, null=True, default=0
+ max_digits=12, decimal_places=2, default=0,
)
salaire_max = models.DecimalField(
- max_digits=12, decimal_places=2, null=True, default=0
+ max_digits=12, decimal_places=2, default=0,
)
indemn_min = models.DecimalField(
- max_digits=12, decimal_places=2, null=True, default=0
+ max_digits=12, decimal_places=2, default=0,
)
indemn_max = models.DecimalField(
- max_digits=12, decimal_places=2, null=True, default=0
+ max_digits=12, decimal_places=2, default=0,
)
autre_min = models.DecimalField(
- max_digits=12, decimal_places=2, null=True, default=0
+ max_digits=12, decimal_places=2, default=0,
)
autre_max = models.DecimalField(
- max_digits=12, decimal_places=2, null=True, default=0
+ max_digits=12, decimal_places=2, default=0,
)
# Comparatifs de rémunération
# Autres Metadata
date_debut = models.DateField(
- u"date de début", help_text=HELP_TEXT_DATE, null=True, blank=True
+ u"date de début", help_text=HELP_TEXT_DATE, null=True, blank=True,
+ db_index=True
)
date_fin = models.DateField(
- u"date de fin", help_text=HELP_TEXT_DATE, null=True, blank=True
+ u"date de fin", help_text=HELP_TEXT_DATE, null=True, blank=True,
+ db_index=True
)
class Meta:
)
return representation
- prefix_implantation = "implantation__region"
+ prefix_implantation = "implantation__zone_administrative"
- def get_regions(self):
- return [self.implantation.region]
+ def get_zones_administratives(self):
+ return [self.implantation.zone_administrative]
def get_devise(self):
vp = ValeurPoint.objects.filter(
UTILISE pour mettre a jour le flag vacant
"""
return [
- d.employe for d in self.rh_dossiers
- .filter(supprime=False)
- .exclude(date_fin__lt=date.today())
+ d.employe
+ for d in self.rh_dossiers.exclude(date_fin__lt=date.today())
]
+reversion.register(Poste, format='xml', follow=[
+ 'rh_financements', 'rh_pieces', 'rh_comparaisons_internes',
+ 'commentaires'
+])
+
POSTE_FINANCEMENT_CHOICES = (
('A', 'A - Frais de personnel'),
Poste, db_column='poste', related_name='rh_financements'
)
+reversion.register(PosteFinancement, format='xml')
+
class PostePiece_(models.Model):
"""
"""
nom = models.CharField(u"Nom", max_length=255)
fichier = models.FileField(
- u"Fichier", upload_to=poste_piece_dispatch, storage=storage_prive
+ u"Fichier", upload_to=poste_piece_dispatch, storage=storage_prive,
+ max_length=255
)
class Meta:
Poste, db_column='poste', related_name='rh_pieces'
)
+reversion.register(PostePiece, format='xml')
+
-class PosteComparaison_(AUFMetadata, DevisableMixin):
+class PosteComparaison_(models.Model, DevisableMixin):
"""
De la même manière qu'un dossier, un poste peut-être comparé à un autre
poste.
Poste, related_name='rh_comparaisons_internes'
)
- objects = NoDeleteManager()
+reversion.register(PosteComparaison, format='xml')
class PosteCommentaire(Commentaire):
Poste, db_column='poste', related_name='commentaires'
)
-
+reversion.register(PosteCommentaire, format='xml')
### EMPLOYÉ/PERSONNE
-class Employe(AUFMetadata):
+class Employe(models.Model):
"""
Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
Dossiers qu'il occupe ou a occupé de Postes.
Cette classe aurait pu avantageusement s'appeler Personne car la notion
d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
"""
+
+ objects = EmployeManager()
+
# Identification
nom = models.CharField(max_length=255)
prenom = models.CharField(u"prénom", max_length=255)
def __unicode__(self):
return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
+ def get_latest_dossier_ordered_by_date_fin_and_principal(self):
+ res = self.rh_dossiers.order_by(
+ '-principal', 'date_fin')
+
+ # Retourne en le premier du queryset si la date de fin est None
+ # Sinon, retourne le plus récent selon la date de fin.
+ first = res[0]
+ if first.date_fin == None:
+ return first
+ else:
+ return res.order_by('-principal', '-date_fin')[0]
+
def civilite(self):
civilite = u''
if self.genre.upper() == u'M':
def dossiers_passes(self):
params = {KEY_STATUT: STATUT_INACTIF, }
- search = RechercheTemporelle(params, self.__class__)
+ search = RechercheTemporelle(params, Dossier)
search.purge_params(params)
q = search.get_q_temporel(self.rh_dossiers)
return self.rh_dossiers.filter(q)
def dossiers_futurs(self):
params = {KEY_STATUT: STATUT_FUTUR, }
- search = RechercheTemporelle(params, self.__class__)
+ search = RechercheTemporelle(params, Dossier)
search.purge_params(params)
q = search.get_q_temporel(self.rh_dossiers)
return self.rh_dossiers.filter(q)
def dossiers_encours(self):
params = {KEY_STATUT: STATUT_ACTIF, }
- search = RechercheTemporelle(params, self.__class__)
+ search = RechercheTemporelle(params, Dossier)
search.purge_params(params)
q = search.get_q_temporel(self.rh_dossiers)
return self.rh_dossiers.filter(q)
+ def dossier_principal_pour_annee(self):
+ return self.dossier_principal(pour_annee=True)
+
+ def dossier_principal(self, pour_annee=False):
+ """
+ Retourne le dossier principal (ou le plus ancien si il y en a
+ plusieurs)
+
+ Si pour_annee == True, retourne le ou les dossiers principaux
+ pour l'annee en cours, sinon, le ou les dossiers principaux
+ pour la journee en cours.
+
+ TODO: (Refactoring possible): Utiliser meme logique dans
+ dae/templatetags/dae.py
+ """
+
+ today = date.today()
+ if pour_annee:
+ year = today.year
+ year_start = date(year, 1, 1)
+ year_end = date(year, 12, 31)
+
+ try:
+ dossier = self.rh_dossiers.filter(
+ (Q(date_debut__lte=year_end, date_fin__isnull=True) |
+ Q(date_debut__isnull=True, date_fin__gte=year_start) |
+ Q(date_debut__lte=year_end, date_fin__gte=year_start) |
+ Q(date_debut__isnull=True, date_fin__isnull=True)) &
+ Q(principal=True)).order_by('date_debut')[0]
+ except IndexError, Dossier.DoesNotExist:
+ dossier = None
+ return dossier
+ else:
+ try:
+ dossier = self.rh_dossiers.filter(
+ (Q(date_debut__lte=today, date_fin__isnull=True) |
+ Q(date_debut__isnull=True, date_fin__gte=today) |
+ Q(date_debut__lte=today, date_fin__gte=today) |
+ Q(date_debut__isnull=True, date_fin__isnull=True)) &
+ Q(principal=True)).order_by('date_debut')[0]
+ except IndexError, Dossier.DoesNotExist:
+ dossier = None
+ return dossier
+
+
def postes_encours(self):
postes_encours = set()
for d in self.dossiers_encours():
Idée derrière :
si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
"""
+ # DEPRECATED : on a maintenant Dossier.principal
poste = Poste.objects.none()
try:
poste = self.dossiers_encours().order_by('date_debut')[0].poste
pass
return poste
- prefix_implantation = "rh_dossiers__poste__implantation__region"
+ prefix_implantation = \
+ "rh_dossiers__poste__implantation__zone_administrative"
+
+ def get_zones_administratives(self):
+ return [
+ d.poste.implantation.zone_administrative
+ for d in self.dossiers.all()
+ ]
- def get_regions(self):
- regions = []
- for d in self.dossiers.all():
- regions.append(d.poste.implantation.region)
- return regions
+reversion.register(Employe, format='xml', follow=[
+ 'pieces', 'commentaires', 'ayantdroits'
+])
class EmployePiece(models.Model):
)
nom = models.CharField(max_length=255)
fichier = models.FileField(
- u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive
+ u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive,
+ max_length=255
)
class Meta:
def __unicode__(self):
return u'%s' % (self.nom)
+reversion.register(EmployePiece, format='xml')
+
class EmployeCommentaire(Commentaire):
employe = models.ForeignKey(
- 'Employe', db_column='employe', related_name='+'
+ 'Employe', db_column='employe', related_name='commentaires'
)
class Meta:
verbose_name = u"Employé commentaire"
verbose_name_plural = u"Employé commentaires"
+reversion.register(EmployeCommentaire, format='xml')
+
LIEN_PARENTE_CHOICES = (
('Conjoint', 'Conjoint'),
)
-class AyantDroit(AUFMetadata):
+class AyantDroit(models.Model):
"""
Personne en relation avec un Employe.
"""
def __unicode__(self):
return u'%s %s' % (self.nom.upper(), self.prenom, )
- prefix_implantation = "employe__dossiers__poste__implantation__region"
+ prefix_implantation = \
+ "employe__dossiers__poste__implantation__zone_administrative"
+
+ def get_zones_administratives(self):
+ return [
+ d.poste.implantation.zone_administrative
+ for d in self.employe.dossiers.all()
+ ]
- def get_regions(self):
- regions = []
- for d in self.employe.dossiers.all():
- regions.append(d.poste.implantation.region)
- return regions
+reversion.register(AyantDroit, format='xml', follow=['commentaires'])
class AyantDroitCommentaire(Commentaire):
ayant_droit = models.ForeignKey(
- 'AyantDroit', db_column='ayant_droit', related_name='+'
+ 'AyantDroit', db_column='ayant_droit', related_name='commentaires'
)
+reversion.register(AyantDroitCommentaire, format='xml')
+
### DOSSIER
)
-class Dossier_(AUFMetadata, DevisableMixin):
+class Dossier_(DateActiviteMixin, models.Model, DevisableMixin,):
"""
Le Dossier regroupe les informations relatives à l'occupation
d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
)
# Occupation du Poste par cet Employe (anciennement "mandat")
- date_debut = models.DateField(u"date de début d'occupation de poste")
+ date_debut = models.DateField(
+ u"date de début d'occupation de poste", db_index=True
+ )
date_fin = models.DateField(
- u"Date de fin d'occupation de poste", null=True, blank=True
+ u"Date de fin d'occupation de poste", null=True, blank=True,
+ db_index=True
)
+ # Meta-data:
+ est_cadre = models.BooleanField(
+ u"Est un cadre?",
+ default=False,
+ )
+
# Comptes
- # TODO?
+ compte_compta = models.CharField(max_length=10, default='aucun',
+ verbose_name=u'Compte comptabilité',
+ choices=COMPTE_COMPTA_CHOICES)
+ compte_courriel = models.BooleanField()
class Meta:
abstract = True
poste = self.poste.nom_feminin
return u'%s - %s' % (self.employe, poste)
- prefix_implantation = "poste__implantation__region"
+ prefix_implantation = "poste__implantation__zone_administrative"
- def get_regions(self):
- return [self.poste.implantation.region]
+ def get_zones_administratives(self):
+ return [self.poste.implantation.zone_administrative]
def remunerations(self):
key = "%s_remunerations" % self._meta.app_label
total += r.montant_euros()
return total
+ def premier_contrat(self):
+ """contrat avec plus petite date de début"""
+ try:
+ contrat = self.rh_contrats.exclude(date_debut=None) \
+ .order_by('date_debut')[0]
+ except IndexError, Contrat.DoesNotExist:
+ contrat = None
+ return contrat
+
+ def dernier_contrat(self):
+ """contrat avec plus grande date de fin"""
+ try:
+ contrat = self.rh_contrats.exclude(date_debut=None) \
+ .order_by('-date_debut')[0]
+ except IndexError, Contrat.DoesNotExist:
+ contrat = None
+ return contrat
+
def actif(self):
today = date.today()
return (self.date_debut is None or self.date_debut <= today) \
u"Ce dossier est pour le principal poste occupé par l'employé"
)
)
+
+
+reversion.register(Dossier, format='xml', follow=[
+ 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
+ 'rh_contrats', 'commentaires'
+])
+
+
+class RHDossierClassementRecord(models.Model):
+ classement = models.ForeignKey(
+ 'Classement',
+ related_name='classement_records',
+ )
+ dossier = models.ForeignKey(
+ 'Dossier',
+ related_name='classement_records',
+ )
+ date_debut = models.DateField(
+ u"date de début",
+ help_text=HELP_TEXT_DATE,
+ null=True,
+ blank=True,
+ db_index=True
+ )
+ date_fin = models.DateField(
+ u"date de fin",
+ help_text=HELP_TEXT_DATE,
+ null=True,
+ blank=True,
+ db_index=True
+ )
+ commentaire = models.CharField(
+ max_length=2048,
+ blank=True,
+ null=True,
+ default='',
+ )
+
+ def __unicode__(self):
+ return self.classement.__unicode__()
+
+ class Meta:
+ verbose_name = u"Element d'historique de classement"
+ verbose_name_plural = u"Historique de classement"
+
+ @classmethod
+ def post_save_handler(cls,
+ sender,
+ instance,
+ created,
+ using,
+ **kw):
+
+ today = date.today()
+ previous_record = None
+ previous_classement = None
+ has_changed = False
+
+ # Premièrement, pour les nouvelles instances:
+ if created:
+ if not instance.classement:
+ return
+ else:
+ cls.objects.create(
+ date_debut=instance.date_debut,
+ classement=instance.classement,
+ dossier=instance,
+ )
+ return
+
+ # Deuxièmement, pour les instances existantes:
+
+ # Détermine si:
+ # 1. Est-ce que le classement a changé?
+ # 2. Est-ce qu'une historique de classement existe déjà
+ try:
+ previous_record = cls.objects.get(
+ dossier=instance,
+ classement=instance.before_save.classement,
+ date_fin=None,
+ )
+ except cls.DoesNotExist:
+ if instance.before_save.classement:
+ # Il était censé avoir une historique de classement
+ # donc on le créé.
+ previous_record = cls.objects.create(
+ date_debut=instance.before_save.date_debut,
+ classement=instance.before_save.classement,
+ dossier=instance,
+ )
+ previous_classement = instance.before_save.classement
+ except MultipleObjectsReturned:
+ qs = cls.objects.filter(
+ dossier=instance,
+ classement=instance.before_save.classement,
+ date_fin=None,
+ )
+ latest = qs.latest('date_debut')
+ qs.exclude(id=latest.id).update(date_fin=today)
+ previous_record = latest
+ previous_classement = latest.classement
+ else:
+ previous_classement = previous_record.classement
+
+ has_changed = (
+ instance.classement !=
+ previous_classement
+ )
+
+ # Cas aucun changement:
+ if not has_changed:
+ return
+
+ else:
+ # Classement a changé
+ if previous_record:
+ previous_record.date_fin = today
+ previous_record.save()
+
+ if instance.classement:
+ cls.objects.create(
+ date_debut=today,
+ classement=instance.classement,
+ dossier=instance,
+ )
class DossierPiece_(models.Model):
"""
nom = models.CharField(max_length=255)
fichier = models.FileField(
- upload_to=dossier_piece_dispatch, storage=storage_prive
+ upload_to=dossier_piece_dispatch, storage=storage_prive,
+ max_length=255
)
class Meta:
Dossier, db_column='dossier', related_name='rh_dossierpieces'
)
-
+reversion.register(DossierPiece, format='xml')
class DossierCommentaire(Commentaire):
dossier = models.ForeignKey(
Dossier, db_column='dossier', related_name='commentaires'
)
+reversion.register(DossierCommentaire, format='xml')
+
class DossierComparaison_(models.Model, DevisableMixin):
"""
Dossier, related_name='rh_comparaisons'
)
+reversion.register(DossierComparaison, format='xml')
+
### RÉMUNÉRATION
-class RemunerationMixin(AUFMetadata):
+class RemunerationMixin(models.Model):
# Identification
type = models.ForeignKey(
null=True, blank=True
)
montant = models.DecimalField(
- null=True, blank=True,
- default=0, max_digits=12, decimal_places=2
+ null=True, blank=True, max_digits=12, decimal_places=2
) # Annuel (12 mois, 52 semaines, 364 jours?)
devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
commentaire = models.CharField(max_length=255, null=True, blank=True)
# date_debut = anciennement date_effectif
- date_debut = models.DateField(u"date de début", null=True, blank=True)
- date_fin = models.DateField(u"date de fin", null=True, blank=True)
+ date_debut = models.DateField(
+ u"date de début", null=True, blank=True, db_index=True
+ )
+ date_fin = models.DateField(
+ u"date de fin", null=True, blank=True, db_index=True
+ )
class Meta:
abstract = True
pour un Dossier. Si un Evenement existe, utiliser la structure de
rémunération EvenementRemuneration de cet événement.
"""
+ objects = RemunerationManager()
+
+ @staticmethod
+ def find_yearly_range(from_date, to_date, year):
+ today = date.today()
+ year = year or date.today().year
+ year_start = date(year, 1, 1)
+ year_end = date(year, 12, 31)
+
+ def constrain_to_year(*dates):
+ """
+ S'assure que les dates soient dans le range year_start a
+ year_end
+ """
+ return [min(max(year_start, d), year_end)
+ for d in dates]
+
+ start_date = max(
+ from_date or year_start, year_start)
+ end_date = min(
+ to_date or year_end, year_end)
+
+ start_date, end_date = constrain_to_year(start_date, end_date)
+
+ jours_annee = (year_end - year_start).days
+ jours_dates = (end_date - start_date).days
+ factor = Decimal(str(jours_dates)) / Decimal(str(jours_annee))
+
+ return start_date, end_date, factor
+
+
+ def montant_ajuste_euros(self, annee=None):
+ """
+ Le montant ajusté représente le montant annuel, ajusté sur la
+ période de temps travaillée, multipliée par le ratio de temps
+ travaillé (en rapport au temps plein).
+ """
+ date_debut, date_fin, factor = self.find_yearly_range(
+ self.date_debut,
+ self.date_fin,
+ annee,
+ )
+ montant_euros = Decimal(str(self.montant_euros_float()) or '0')
+
+ if self.type.nature_remuneration != u'Accessoire':
+ dossier = getattr(self, 'dossier', None)
+ if not dossier:
+ """
+ Dans le cas d'un DossierComparaisonRemuneration, il
+ n'y a plus de reference au dossier.
+ """
+ regime_travail = REGIME_TRAVAIL_DEFAULT
+ else:
+ regime_travail = self.dossier.regime_travail
+ return (montant_euros * factor *
+ regime_travail / 100)
+ else:
+ return montant_euros
+
def montant_mois(self):
return round(self.montant / 12, 2)
Dossier, db_column='dossier', related_name='rh_remunerations'
)
+reversion.register(Remuneration, format='xml')
-### CONTRATS
-
-class ContratManager(NoDeleteManager):
- def get_query_set(self):
- return super(ContratManager, self).get_query_set() \
- .select_related('dossier', 'dossier__poste')
+### CONTRATS
-class Contrat_(AUFMetadata):
+class Contrat_(models.Model):
"""
Document juridique qui encadre la relation de travail d'un Employe
pour un Poste particulier. Pour un Dossier (qui documente cette
'TypeContrat', db_column='type_contrat',
verbose_name=u'type de contrat', related_name='+'
)
- date_debut = models.DateField(u"date de début")
- date_fin = models.DateField(u"date de fin", null=True, blank=True)
+ date_debut = models.DateField(
+ u"date de début", db_index=True
+ )
+ date_fin = models.DateField(
+ u"date de fin", null=True, blank=True, db_index=True
+ )
fichier = models.FileField(
upload_to=contrat_dispatch, storage=storage_prive, null=True,
- blank=True
+ blank=True, max_length=255
)
class Meta:
Dossier, db_column='dossier', related_name='rh_contrats'
)
+reversion.register(Contrat, format='xml')
+
### RÉFÉRENCES RH
-class CategorieEmploi(AUFMetadata):
+class CategorieEmploi(models.Model):
"""
Catégorie utilisée dans la gestion des Postes.
Catégorie supérieure à TypePoste.
def __unicode__(self):
return self.nom
+reversion.register(CategorieEmploi, format='xml')
+
class FamilleProfessionnelle(models.Model):
"""
def __unicode__(self):
return self.nom
+reversion.register(FamilleProfessionnelle, format='xml')
+
-class TypePoste(AUFMetadata):
+class TypePoste(Archivable):
"""
Catégorie de Poste.
"""
def __unicode__(self):
return u'%s' % (self.nom)
+reversion.register(TypePoste, format='xml')
+
+
TYPE_PAIEMENT_CHOICES = (
(u'Régulier', u'Régulier'),
(u'Ponctuel', u'Ponctuel'),
)
NATURE_REMUNERATION_CHOICES = (
- (u'Accessoire', u'Accessoire'),
- (u'Charges', u'Charges'),
- (u'Indemnité', u'Indemnité'),
- (u'RAS', u'Rémunération autre source'),
(u'Traitement', u'Traitement'),
+ (u'Indemnité', u'Indemnités autres'),
+ (u'Charges', u'Charges patronales'),
+ (u'Accessoire', u'Accessoires'),
+ (u'RAS', u'Rémunération autre source'),
)
-class TypeRemuneration(AUFMetadata):
+class TypeRemuneration(Archivable):
"""
Catégorie de Remuneration.
"""
- objects = TypeRemunerationManager()
+
+ objects = models.Manager()
+ sans_archives = ArchivableManager()
nom = models.CharField(max_length=255)
type_paiement = models.CharField(
u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
)
+
nature_remuneration = models.CharField(
u"nature de la rémunération", max_length=30,
choices=NATURE_REMUNERATION_CHOICES
)
- archive = models.BooleanField(verbose_name=u"Archivé", default=False)
class Meta:
ordering = ['nom']
verbose_name_plural = u"Types de rémunération"
def __unicode__(self):
- if self.archive:
- archive = u"(archivé)"
- else:
- archive = ""
- return u'%s %s' % (self.nom, archive)
+ return self.nom
+
+reversion.register(TypeRemuneration, format='xml')
-class TypeRevalorisation(AUFMetadata):
+class TypeRevalorisation(Archivable):
"""
Justification du changement de la Remuneration.
(Actuellement utilisé dans aucun traitement informatique.)
def __unicode__(self):
return u'%s' % (self.nom)
+reversion.register(TypeRevalorisation, format='xml')
+
-class Service(AUFMetadata):
+class Service(Archivable):
"""
Unité administrative où les Postes sont rattachés.
"""
- objects = ServiceManager()
-
- archive = models.BooleanField(verbose_name=u"Archivé", default=False)
nom = models.CharField(max_length=255)
class Meta:
ordering = ['nom']
- verbose_name = u"Service"
- verbose_name_plural = u"Services"
+ verbose_name = u"service"
+ verbose_name_plural = u"services"
def __unicode__(self):
- if self.archive:
- archive = u"(archivé)"
- else:
- archive = ""
- return u'%s %s' % (self.nom, archive)
+ return self.nom
+
+reversion.register(Service, format='xml')
TYPE_ORGANISME_CHOICES = (
)
-class OrganismeBstg(AUFMetadata):
+class OrganismeBstg(models.Model):
"""
Organisation d'où provient un Employe mis à disposition (MAD) de
ou détaché (DET) à l'AUF à titre gratuit.
def __unicode__(self):
return u'%s (%s)' % (self.nom, self.get_type_display())
- prefix_implantation = "pays__region"
-
- def get_regions(self):
- return [self.pays.region]
+reversion.register(OrganismeBstg, format='xml')
-class Statut(AUFMetadata):
+class Statut(Archivable):
"""
Statut de l'Employe dans le cadre d'un Dossier particulier.
"""
def __unicode__(self):
return u'%s : %s' % (self.code, self.nom)
+reversion.register(Statut, format='xml')
+
TYPE_CLASSEMENT_CHOICES = (
('S', 'S -Soutien'),
Ordonner les spcéfiquement les classements.
"""
def get_query_set(self):
- qs = super(self.__class__, self).get_query_set()
+ qs = super(ClassementManager, self).get_query_set()
qs = qs.extra(select={
'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
})
return qs.all()
-class Classement_(AUFMetadata):
+class ClassementArchivableManager(ClassementManager,
+ ArchivableManager):
+ pass
+
+
+class Classement_(Archivable):
"""
Éléments de classement de la
"Grille générique de classement hiérarchique".
salaire de base = coefficient * valeur du point de l'Implantation du Poste
"""
objects = ClassementManager()
+ sans_archives = ClassementArchivableManager()
# Identification
type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
echelon = models.IntegerField(u"échelon", blank=True, default=0)
degre = models.IntegerField(u"degré", blank=True, default=0)
- coefficient = models.FloatField(u"coefficient", default=0, null=True)
+ coefficient = models.FloatField(u"coefficient", blank=True, null=True)
# Méta
# annee # au lieu de date_debut et date_fin
class Classement(Classement_):
__doc__ = Classement_.__doc__
+reversion.register(Classement, format='xml')
+
-class TauxChange_(AUFMetadata):
+class TauxChange_(models.Model):
"""
Taux de change de la devise vers l'euro (EUR)
pour chaque année budgétaire.
ordering = ['-annee', 'devise__code']
verbose_name = u"Taux de change"
verbose_name_plural = u"Taux de change"
+ unique_together = ('devise', 'annee')
def __unicode__(self):
return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
class TauxChange(TauxChange_):
__doc__ = TauxChange_.__doc__
+reversion.register(TauxChange, format='xml')
-class ValeurPointManager(NoDeleteManager):
+
+class ValeurPointManager(models.Manager):
def get_query_set(self):
return super(ValeurPointManager, self).get_query_set() \
- .select_related('devise', 'implantation')
+ .select_related('devise', 'implantation')
-class ValeurPoint_(AUFMetadata):
+class ValeurPoint_(models.Model):
"""
Utile pour connaître, pour un Dossier, le salaire de base théorique lié
au classement dans la grille. La ValeurPoint s'obtient par l'implantation
salaire de base = coefficient * valeur du point de l'Implantation du Poste
"""
+ objects = models.Manager()
actuelles = ValeurPointManager()
valeur = models.FloatField(null=True)
abstract = True
verbose_name = u"Valeur du point"
verbose_name_plural = u"Valeurs du point"
+ unique_together = ('implantation', 'annee')
def __unicode__(self):
return u'%s %s %s [%s] %s' % (
class ValeurPoint(ValeurPoint_):
__doc__ = ValeurPoint_.__doc__
+reversion.register(ValeurPoint, format='xml')
+
-class Devise(AUFMetadata):
+class Devise(Archivable):
"""
Devise monétaire.
"""
- objects = DeviseManager()
-
- archive = models.BooleanField(verbose_name=u"Archivé", default=False)
code = models.CharField(max_length=10, unique=True)
nom = models.CharField(max_length=255)
class Meta:
ordering = ['code']
- verbose_name = u"Devise"
- verbose_name_plural = u"Devises"
+ verbose_name = u"devise"
+ verbose_name_plural = u"devises"
def __unicode__(self):
return u'%s - %s' % (self.code, self.nom)
+reversion.register(Devise, format='xml')
-class TypeContrat(AUFMetadata):
+
+class TypeContrat(Archivable):
"""
Type de contrat.
"""
def __unicode__(self):
return u'%s' % (self.nom)
+reversion.register(TypeContrat, format='xml')
+
### AUTRES
class ResponsableImplantationProxy(ref.Implantation):
+ def save(self):
+ pass
+
class Meta:
managed = False
proxy = True
ordering = ['implantation__nom']
verbose_name = "Responsable d'implantation"
verbose_name_plural = "Responsables d'implantation"
+
+reversion.register(ResponsableImplantation, format='xml')
+
+
+class UserProfile(models.Model):
+ user = models.OneToOneField(User, related_name='profile')
+ zones_administratives = models.ManyToManyField(
+ ref.ZoneAdministrative,
+ related_name='profiles'
+ )
+ class Meta:
+ verbose_name = "Permissions sur zones administratives"
+ verbose_name_plural = "Permissions sur zones administratives"
+
+ def __unicode__(self):
+ return self.user.__unicode__()
+
+reversion.register(UserProfile, format='xml')
+
+
+
+TYPES_CHANGEMENT = (
+ ('NO', 'Arrivée'),
+ ('MO', 'Mobilité'),
+ ('DE', 'Départ'),
+ )
+
+
+class ChangementPersonnelNotifications(models.Model):
+ class Meta:
+ verbose_name = u"Destinataire pour notices de mouvement de personnel"
+ verbose_name_plural = u"Destinataires pour notices de mouvement de personnel"
+
+ type = models.CharField(
+ max_length=2,
+ choices = TYPES_CHANGEMENT,
+ unique=True,
+ )
+
+ destinataires = models.ManyToManyField(
+ ref.Employe,
+ related_name='changement_notifications',
+ )
+
+ def __unicode__(self):
+ return '%s: %s' % (
+ self.get_type_display(), ','.join(
+ self.destinataires.all().values_list(
+ 'courriel', flat=True))
+ )
+
+
+class ChangementPersonnel(models.Model):
+ """
+ Une notice qui enregistre un changement de personnel, incluant:
+
+ * Nouveaux employés
+ * Mouvement de personnel
+ * Départ d'employé
+ """
+
+ class Meta:
+ verbose_name = u"Mouvement de personnel"
+ verbose_name_plural = u"Mouvements de personnel"
+
+ def __unicode__(self):
+ return '%s: %s' % (self.dossier.__unicode__(),
+ self.get_type_display())
+
+ @classmethod
+ def create_changement(cls, dossier, type):
+ # If this employe has existing Changement, set them to invalid.
+ cls.objects.filter(dossier__employe=dossier.employe).update(valide=False)
+
+ # Create a new one.
+ cls.objects.create(
+ dossier=dossier,
+ type=type,
+ valide=True,
+ communique=False,
+ )
+
+
+ @classmethod
+ def post_save_handler(cls,
+ sender,
+ instance,
+ created,
+ using,
+ **kw):
+
+ # This defines the time limit used when checking in previous
+ # files to see if an employee if new. Basically, if emloyee
+ # left his position new_file.date_debut -
+ # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
+ # if a new file is created for this employee, he will bec
+ # onsidered "NEW" and a notice will be created to this effect.
+ NEW_EMPLOYE_THRESHOLD = datetime.timedelta(7) # 7 days.
+
+ other_dossier_qs = instance.employe.rh_dossiers.exclude(
+ id=instance.id)
+ dd = instance.date_debut
+ df = instance.date_fin
+ today = date.today()
+
+ # Here, verify differences between the instance, before and
+ # after the save.
+ df_has_changed = False
+
+ if created:
+ if df != None:
+ df_has_changed = True
+ else:
+ df_has_changed = (df != instance.before_save.date_fin and
+ df != None)
+
+
+ # VERIFICATIONS:
+
+ # Date de fin est None et c'est une nouvelle instance de
+ # Dossier
+ if not df and created:
+ # QS for finding other dossiers with a date_fin of None OR
+ # with a date_fin >= to this dossier's date_debut
+ exists_recent_file_qs = other_dossier_qs.filter(
+ Q(date_fin__isnull=True) |
+ Q(date_fin__gte=dd - NEW_EMPLOYE_THRESHOLD)
+ )
+
+ # 1. If existe un Dossier récent
+ if exists_recent_file_qs.count() > 0:
+ cls.create_changement(
+ instance,
+ 'MO',
+ )
+ # 2. Il n'existe un Dossier récent, et c'est une nouvelle
+ # instance de Dossier:
+ else:
+ cls.create_changement(
+ instance,
+ 'NO',
+ )
+
+ elif not df and not created and cls.objects.filter(
+ valide=True,
+ date_creation__gte=today - NEW_EMPLOYE_THRESHOLD,
+ type='DE',
+ ).count() > 0:
+ cls.create_changement(
+ instance,
+ 'MO',
+ )
+
+ # Date de fin a été modifiée:
+ if df_has_changed:
+ # QS for other active files (date_fin == None), excludes
+ # instance.
+ exists_active_files_qs = other_dossier_qs.filter(
+ Q(date_fin__isnull=True))
+
+ # 3. Date de fin a été modifiée et il n'existe aucun autre
+ # dossier actifs: Depart
+ if exists_active_files_qs.count() == 0:
+ cls.create_changement(
+ instance,
+ 'DE',
+ )
+ # 4. Dossier a une nouvelle date de fin par contre
+ # d'autres dossiers actifs existent déjà: Mouvement
+ else:
+ cls.create_changement(
+ instance,
+ 'MO',
+ )
+
+
+ dossier = models.ForeignKey(
+ Dossier,
+ related_name='mouvements',
+ )
+
+ valide = models.BooleanField(default=True)
+ date_creation = models.DateTimeField(
+ auto_now_add=True)
+ communique = models.BooleanField(
+ u'Communiqué',
+ default=False,
+ )
+ date_communication = models.DateTimeField(
+ null=True,
+ blank=True,
+ )
+
+ type = models.CharField(
+ max_length=2,
+ choices = TYPES_CHANGEMENT,
+ )
+
+reversion.register(ChangementPersonnel, format='xml')
+
+
+def dossier_pre_save_handler(sender,
+ instance,
+ using,
+ **kw):
+ # Store a copy of the model before save is called.
+ if instance.pk is not None:
+ instance.before_save = Dossier.objects.get(pk=instance.pk)
+ else:
+ instance.before_save = None
+
+
+# Connect a pre_save handler that assigns a copy of the model as an
+# attribute in order to compare it in post_save.
+pre_save.connect(dossier_pre_save_handler, sender=Dossier)
+
+post_save.connect(ChangementPersonnel.post_save_handler, sender=Dossier)
+post_save.connect(RHDossierClassementRecord.post_save_handler, sender=Dossier)
+
+