Augmenté la taille des FileField
[auf_rh_dae.git] / project / rh / models.py
index 67cd2b2..3c7a538 100644 (file)
@@ -8,19 +8,37 @@ 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 project.rh.change_list import \
         RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \
         STATUT_FUTUR
-from project.rh.managers import \
-        PosteManager, DossierManager, DossierComparaisonManager, \
-        PosteComparaisonManager, TypeRemunerationManager, EmployeManager
+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"
@@ -35,6 +53,9 @@ storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
                             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
@@ -63,11 +84,18 @@ def contrat_dispatch(instance, filename):
     return path
 
 
-class ArchivableManager(models.Manager):
-
-    def get_query_set(self):
-        return super(ArchivableManager, self).get_query_set() \
-                .filter(archive=False)
+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):
@@ -95,31 +123,21 @@ class DevisableMixin(object):
             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)
+        taux = TauxChange.objects.filter(devise=devise, annee__lte=annee) \
+                .order_by('-annee')
+        return taux[0].taux
 
-        if len(taux) == 0:
-            raise Exception(
-                u"Pas de taux pour %s en %s" % (devise.code, annee)
-            )
-
-        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(models.Model):
@@ -148,7 +166,7 @@ POSTE_APPEL_CHOICES = (
 )
 
 
-class Poste_(models.Model):
+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éé.
@@ -238,22 +256,22 @@ class Poste_(models.Model):
         'Devise', db_column='devise_max', null=True, related_name='+'
     )
     salaire_min = models.DecimalField(
-        max_digits=12, decimal_places=2, null=True, blank=True
+        max_digits=12, decimal_places=2, default=0,
     )
     salaire_max = models.DecimalField(
-        max_digits=12, decimal_places=2, null=True, blank=True
+        max_digits=12, decimal_places=2, default=0,
     )
     indemn_min = models.DecimalField(
-        max_digits=12, decimal_places=2, null=True, blank=True
+        max_digits=12, decimal_places=2, default=0,
     )
     indemn_max = models.DecimalField(
-        max_digits=12, decimal_places=2, null=True, blank=True
+        max_digits=12, decimal_places=2, default=0,
     )
     autre_min = models.DecimalField(
-        max_digits=12, decimal_places=2, null=True, blank=True
+        max_digits=12, decimal_places=2, default=0,
     )
     autre_max = models.DecimalField(
-        max_digits=12, decimal_places=2, null=True, blank=True
+        max_digits=12, decimal_places=2, default=0,
     )
 
     # Comparatifs de rémunération
@@ -297,10 +315,12 @@ class Poste_(models.Model):
 
     # 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:
@@ -316,10 +336,10 @@ class Poste_(models.Model):
         )
         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(
@@ -408,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:
@@ -465,7 +486,6 @@ class PosteCommentaire(Commentaire):
 
 reversion.register(PosteCommentaire, format='xml')
 
-
 ### EMPLOYÉ/PERSONNE
 
 class Employe(models.Model):
@@ -536,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':
@@ -554,25 +586,70 @@ class Employe(models.Model):
 
     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():
@@ -585,6 +662,7 @@ class Employe(models.Model):
         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
@@ -592,13 +670,14 @@ class Employe(models.Model):
             pass
         return poste
 
-    prefix_implantation = "rh_dossiers__poste__implantation__region"
+    prefix_implantation = \
+            "rh_dossiers__poste__implantation__zone_administrative"
 
-    def get_regions(self):
-        regions = []
-        for d in self.dossiers.all():
-            regions.append(d.poste.implantation.region)
-        return regions
+    def get_zones_administratives(self):
+        return [
+            d.poste.implantation.zone_administrative
+            for d in self.dossiers.all()
+        ]
 
 reversion.register(Employe, format='xml', follow=[
     'pieces', 'commentaires', 'ayantdroits'
@@ -616,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:
@@ -689,13 +769,14 @@ class AyantDroit(models.Model):
     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_regions(self):
-        regions = []
-        for d in self.employe.dossiers.all():
-            regions.append(d.poste.implantation.region)
-        return regions
+    def get_zones_administratives(self):
+        return [
+            d.poste.implantation.zone_administrative
+            for d in self.employe.dossiers.all()
+        ]
 
 reversion.register(AyantDroit, format='xml', follow=['commentaires'])
 
@@ -722,7 +803,7 @@ COMPTE_COMPTA_CHOICES = (
 )
 
 
-class Dossier_(models.Model, 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é
@@ -774,13 +855,25 @@ class Dossier_(models.Model, DevisableMixin):
     )
 
     # 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
@@ -804,10 +897,10 @@ class Dossier_(models.Model, DevisableMixin):
             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
@@ -988,6 +1081,24 @@ class Dossier_(models.Model, DevisableMixin):
             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) \
@@ -1012,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',
@@ -1019,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é).
@@ -1026,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:
@@ -1044,7 +1276,6 @@ class DossierPiece(DossierPiece_):
 
 reversion.register(DossierPiece, format='xml')
 
-
 class DossierCommentaire(Commentaire):
     dossier = models.ForeignKey(
         Dossier, db_column='dossier', related_name='commentaires'
@@ -1107,8 +1338,12 @@ class RemunerationMixin(models.Model):
     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
@@ -1124,7 +1359,66 @@ class Remuneration_(RemunerationMixin, DevisableMixin):
     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)
 
@@ -1157,13 +1451,6 @@ reversion.register(Remuneration, format='xml')
 
 ### CONTRATS
 
-class ContratManager(models.Manager):
-
-    def get_query_set(self):
-        return super(ContratManager, self).get_query_set() \
-                .select_related('dossier', 'dossier__poste')
-
-
 class Contrat_(models.Model):
     """
     Document juridique qui encadre la relation de travail d'un Employe
@@ -1175,11 +1462,15 @@ class Contrat_(models.Model):
         '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:
@@ -1237,7 +1528,7 @@ class FamilleProfessionnelle(models.Model):
 reversion.register(FamilleProfessionnelle, format='xml')
 
 
-class TypePoste(models.Model):
+class TypePoste(Archivable):
     """
     Catégorie de Poste.
     """
@@ -1272,11 +1563,11 @@ TYPE_PAIEMENT_CHOICES = (
 )
 
 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'),
 )
 
 
@@ -1284,12 +1575,15 @@ 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
@@ -1306,7 +1600,7 @@ class TypeRemuneration(Archivable):
 reversion.register(TypeRemuneration, format='xml')
 
 
-class TypeRevalorisation(models.Model):
+class TypeRevalorisation(Archivable):
     """
     Justification du changement de la Remuneration.
     (Actuellement utilisé dans aucun traitement informatique.)
@@ -1369,15 +1663,10 @@ class OrganismeBstg(models.Model):
     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(models.Model):
+class Statut(Archivable):
     """
     Statut de l'Employe dans le cadre d'un Dossier particulier.
     """
@@ -1420,7 +1709,7 @@ 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")'
         })
@@ -1428,7 +1717,12 @@ class ClassementManager(models.Manager):
         return qs.all()
 
 
-class Classement_(models.Model):
+class ClassementArchivableManager(ClassementManager,
+                                  ArchivableManager):
+    pass
+
+
+class Classement_(Archivable):
     """
     Éléments de classement de la
     "Grille générique de classement hiérarchique".
@@ -1439,6 +1733,7 @@ class Classement_(models.Model):
     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)
@@ -1481,6 +1776,7 @@ class TauxChange_(models.Model):
         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)
@@ -1496,7 +1792,7 @@ 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_(models.Model):
@@ -1524,6 +1820,7 @@ class ValeurPoint_(models.Model):
         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' % (
@@ -1556,7 +1853,7 @@ class Devise(Archivable):
 reversion.register(Devise, format='xml')
 
 
-class TypeContrat(models.Model):
+class TypeContrat(Archivable):
     """
     Type de contrat.
     """
@@ -1582,6 +1879,7 @@ class ResponsableImplantationProxy(ref.Implantation):
         pass
 
     class Meta:
+        managed = False
         proxy = True
         verbose_name = u"Responsable d'implantation"
         verbose_name_plural = u"Responsables d'implantation"
@@ -1610,3 +1908,221 @@ class ResponsableImplantation(models.Model):
         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)
+
+