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 project.rh.change_list import \
)
+TWOPLACES = Decimal('0.01')
+
from project.rh.validators import validate_date_passee
# import pour relocaliser le modèle selon la convention (models.py pour
.order_by('-annee')
return taux[0].taux
- 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(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:
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':
q = search.get_q_temporel(self.rh_dossiers)
return self.rh_dossiers.filter(q)
- def dossier_principal(self):
+ 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
"""
- try:
- dossier = self.rh_dossiers \
- .filter(principal=True).order_by('date_debut')[0]
- except IndexError, Dossier.DoesNotExist:
- dossier = None
- return dossier
+
+ 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()
)
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:
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
u"Ce dossier est pour le principal poste occupé par l'employé"
)
)
+
reversion.register(Dossier, format='xml', follow=[
'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
])
+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):
"""
Documents relatifs au Dossier (à l'occupation de ce poste par employé).
"""
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:
"""
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)
)
fichier = models.FileField(
upload_to=contrat_dispatch, storage=storage_prive, null=True,
- blank=True
+ blank=True, max_length=255
)
class Meta:
)
NATURE_REMUNERATION_CHOICES = (
- (u'Accessoire', u'Traitement ponctuel'),
+ (u'Traitement', u'Traitement'),
+ (u'Indemnité', u'Indemnités autres'),
(u'Charges', u'Charges patronales'),
- (u'Indemnité', u'Indemnité'),
+ (u'Accessoire', u'Accessoires'),
(u'RAS', u'Rémunération autre source'),
- (u'Traitement', u'Traitement'),
)
Catégorie de Remuneration.
"""
+ 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
)
-class ClassementManager(ArchivableManager):
+class ClassementManager(models.Manager):
"""
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 ClassementArchivableManager(ClassementManager,
+ ArchivableManager):
+ pass
+
+
class Classement_(Archivable):
"""
Éléments de classement de la
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)
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)
+
+