Augmenté la taille des FileField
[auf_rh_dae.git] / project / rh / models.py
index dbc68d0..3c7a538 100644 (file)
@@ -4,27 +4,41 @@ import datetime
 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, EmployeManager, \
-        DossierComparaisonManager, \
-        PosteComparaisonManager, DeviseManager, ServiceManager, \
-        TypeRemunerationManager, RemunerationManager
+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"
@@ -39,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
@@ -67,6 +84,30 @@ def contrat_dispatch(instance, 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):
@@ -82,39 +123,32 @@ 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(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
@@ -132,7 +166,7 @@ POSTE_APPEL_CHOICES = (
 )
 
 
-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éé.
@@ -222,22 +256,22 @@ class Poste_(AUFMetadata):
         '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
@@ -281,10 +315,12 @@ class Poste_(AUFMetadata):
 
     # 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:
@@ -300,10 +336,10 @@ class Poste_(AUFMetadata):
         )
         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(
@@ -335,11 +371,15 @@ class Poste(Poste_):
         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'),
@@ -378,6 +418,8 @@ class PosteFinancement(PosteFinancement_):
         Poste, db_column='poste', related_name='rh_financements'
     )
 
+reversion.register(PosteFinancement, format='xml')
+
 
 class PostePiece_(models.Model):
     """
@@ -386,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:
@@ -402,8 +445,10 @@ class PostePiece(PostePiece_):
         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.
@@ -431,7 +476,7 @@ class PosteComparaison(PosteComparaison_):
         Poste, related_name='rh_comparaisons_internes'
     )
 
-    objects = NoDeleteManager()
+reversion.register(PosteComparaison, format='xml')
 
 
 class PosteCommentaire(Commentaire):
@@ -439,10 +484,11 @@ 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.
@@ -452,7 +498,7 @@ class Employe(AUFMetadata):
     """
 
     objects = EmployeManager()
-    
+
     # Identification
     nom = models.CharField(max_length=255)
     prenom = models.CharField(u"prénom", max_length=255)
@@ -510,6 +556,18 @@ class Employe(AUFMetadata):
     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':
@@ -528,34 +586,69 @@ class Employe(AUFMetadata):
 
     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(self):
-        """Retourne le dossier principal 
-        (ou le plus ancien si il y en a plusieurs)
+    def dossier_principal_pour_annee(self):
+        return self.dossier_principal(pour_annee=True)
+
+    def dossier_principal(self, pour_annee=False):
         """
-        try:
-            dossier = self.rh_dossiers.filter(principal=True).order_by('date_debut')[0]
-        except IndexError, Dossier.DoesNotExist:
-            dossier = None
-        return dossier
+        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()
@@ -577,13 +670,18 @@ class Employe(AUFMetadata):
             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'
+])
 
 
 class EmployePiece(models.Model):
@@ -597,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:
@@ -608,16 +707,20 @@ class EmployePiece(models.Model):
     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'),
@@ -627,7 +730,7 @@ LIEN_PARENTE_CHOICES = (
 )
 
 
-class AyantDroit(AUFMetadata):
+class AyantDroit(models.Model):
     """
     Personne en relation avec un Employe.
     """
@@ -666,20 +769,25 @@ class AyantDroit(AUFMetadata):
     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
 
@@ -695,7 +803,7 @@ COMPTE_COMPTA_CHOICES = (
 )
 
 
-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é
@@ -747,13 +855,25 @@ class Dossier_(AUFMetadata, 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
@@ -777,10 +897,10 @@ class Dossier_(AUFMetadata, 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
@@ -964,15 +1084,17 @@ class Dossier_(AUFMetadata, DevisableMixin):
     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]
+            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]
+            contrat = self.rh_contrats.exclude(date_debut=None) \
+                    .order_by('-date_debut')[0]
         except IndexError, Contrat.DoesNotExist:
             contrat = None
         return contrat
@@ -1001,6 +1123,131 @@ 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',
+    '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):
@@ -1010,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:
@@ -1026,12 +1274,15 @@ class DossierPiece(DossierPiece_):
         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):
     """
@@ -1061,10 +1312,12 @@ class DossierComparaison(DossierComparaison_):
         Dossier, related_name='rh_comparaisons'
     )
 
+reversion.register(DossierComparaison, format='xml')
+
 
 ### RÉMUNÉRATION
 
-class RemunerationMixin(AUFMetadata):
+class RemunerationMixin(models.Model):
 
     # Identification
     type = models.ForeignKey(
@@ -1077,8 +1330,7 @@ class RemunerationMixin(AUFMetadata):
         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='+')
 
@@ -1086,10 +1338,12 @@ class RemunerationMixin(AUFMetadata):
     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)
-
-    objects = RemunerationManager()
+    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
@@ -1105,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)
 
@@ -1133,16 +1446,12 @@ class Remuneration(Remuneration_):
         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
@@ -1153,11 +1462,15 @@ class Contrat_(AUFMetadata):
         '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:
@@ -1175,10 +1488,12 @@ class Contrat(Contrat_):
         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.
@@ -1193,6 +1508,8 @@ class CategorieEmploi(AUFMetadata):
     def __unicode__(self):
         return self.nom
 
+reversion.register(CategorieEmploi, format='xml')
+
 
 class FamilleProfessionnelle(models.Model):
     """
@@ -1208,8 +1525,10 @@ 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.
     """
@@ -1235,35 +1554,40 @@ class TypePoste(AUFMetadata):
     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']
@@ -1271,14 +1595,12 @@ class TypeRemuneration(AUFMetadata):
         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.)
@@ -1293,27 +1615,24 @@ class TypeRevalorisation(AUFMetadata):
     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 = (
@@ -1322,7 +1641,7 @@ 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.
@@ -1344,13 +1663,10 @@ class OrganismeBstg(AUFMetadata):
     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.
     """
@@ -1374,6 +1690,8 @@ class Statut(AUFMetadata):
     def __unicode__(self):
         return u'%s : %s' % (self.code, self.nom)
 
+reversion.register(Statut, format='xml')
+
 
 TYPE_CLASSEMENT_CHOICES = (
     ('S', 'S -Soutien'),
@@ -1391,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")'
         })
@@ -1399,7 +1717,12 @@ class ClassementManager(models.Manager):
         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".
@@ -1410,12 +1733,13 @@ class Classement_(AUFMetadata):
     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
@@ -1434,8 +1758,10 @@ class Classement_(AUFMetadata):
 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.
@@ -1450,6 +1776,7 @@ class TauxChange_(AUFMetadata):
         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)
@@ -1458,15 +1785,17 @@ class TauxChange_(AUFMetadata):
 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
@@ -1475,6 +1804,7 @@ class ValeurPoint_(AUFMetadata):
     salaire de base = coefficient * valeur du point de l'Implantation du Poste
     """
 
+    objects = models.Manager()
     actuelles = ValeurPointManager()
 
     valeur = models.FloatField(null=True)
@@ -1490,6 +1820,7 @@ class ValeurPoint_(AUFMetadata):
         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' % (
@@ -1501,27 +1832,28 @@ class ValeurPoint_(AUFMetadata):
 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.
     """
@@ -1536,6 +1868,8 @@ class TypeContrat(AUFMetadata):
     def __unicode__(self):
         return u'%s' % (self.nom)
 
+reversion.register(TypeContrat, format='xml')
+
 
 ### AUTRES
 
@@ -1545,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"
@@ -1571,3 +1906,223 @@ class ResponsableImplantation(models.Model):
         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)
+
+