X-Git-Url: http://git.auf.org/?p=auf_rh_dae.git;a=blobdiff_plain;f=project%2Frh%2Fmodels.py;h=3c7a53896efbd019ddc3877e1a84b82be6b1bfbc;hp=94c0e48bf102a7a8fcef7313e423a6675c3a3f78;hb=9f6c277ce74205b30ddca1624c727c838644fa6e;hpb=2ae86fc29c82ba2f600a3758265f22acfe46f648 diff --git a/project/rh/models.py b/project/rh/models.py index 94c0e48..3c7a538 100644 --- a/project/rh/models.py +++ b/project/rh/models.py @@ -10,8 +10,10 @@ from auf.django.emploi.models import \ 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 \ @@ -30,6 +32,8 @@ from project.rh.managers 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 @@ -123,14 +127,17 @@ class DevisableMixin(object): .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): @@ -421,7 +428,8 @@ 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: @@ -548,6 +556,18 @@ class Employe(models.Model): 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': @@ -585,17 +605,50 @@ class Employe(models.Model): 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() @@ -642,7 +695,8 @@ 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: @@ -809,8 +863,17 @@ class Dossier_(DateActiviteMixin, models.Model, DevisableMixin,): 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 @@ -1060,6 +1123,7 @@ class Dossier(Dossier_): u"Ce dossier est pour le principal poste occupé par l'employé" ) ) + reversion.register(Dossier, format='xml', follow=[ 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations', @@ -1067,6 +1131,125 @@ reversion.register(Dossier, format='xml', follow=[ ]) +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é). @@ -1074,7 +1257,8 @@ 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: @@ -1177,6 +1361,64 @@ class Remuneration_(RemunerationMixin, DevisableMixin): """ 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) @@ -1228,7 +1470,7 @@ class Contrat_(models.Model): ) fichier = models.FileField( upload_to=contrat_dispatch, storage=storage_prive, null=True, - blank=True + blank=True, max_length=255 ) class Meta: @@ -1321,11 +1563,11 @@ TYPE_PAIEMENT_CHOICES = ( ) 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'), ) @@ -1334,6 +1576,9 @@ class TypeRemuneration(Archivable): 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 @@ -1459,12 +1704,12 @@ TYPE_CLASSEMENT_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")' }) @@ -1472,6 +1717,11 @@ class ClassementManager(ArchivableManager): return qs.all() +class ClassementArchivableManager(ClassementManager, + ArchivableManager): + pass + + class Classement_(Archivable): """ Éléments de classement de la @@ -1483,6 +1733,7 @@ class Classement_(Archivable): 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) @@ -1673,3 +1924,205 @@ class UserProfile(models.Model): 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) + +