Augmenté la taille des FileField
[auf_rh_dae.git] / project / rh / models.py
index 520f30d..3c7a538 100644 (file)
@@ -4,63 +4,115 @@ 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
-import auf.django.references.models as ref
-from validators import validate_date_passee
-from managers import PosteManager, DossierManager, DossierComparaisonManager, \
-    PosteComparaisonManager, DeviseManager, ServiceManager, TypeRemunerationManager
-from change_list import RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, \
-    STATUT_INACTIF, STATUT_FUTUR
+from project.rh.change_list import \
+        RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \
+        STATUT_FUTUR
+from project import groups
+from project.rh.managers import (
+    PosteManager,
+    DossierManager,
+    EmployeManager,
+    DossierComparaisonManager,
+    PosteComparaisonManager,
+    ContratManager,
+    RemunerationManager,
+    ArchivableManager,
+    )
 
 
-# Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
-# Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
-def app_context():
-    import inspect;
-    models_stack = [s[1].split('/')[-2] for s in inspect.stack() if s[1].endswith('models.py')]
-    return models_stack[-1]
+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"
 REGIME_TRAVAIL_DEFAULT = Decimal('100.00')
 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = Decimal('35.00')
-REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT = "Saisir le nombre d'heure de travail à temps complet (100%), sans tenir compte du régime de travail"
+REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT = \
+        "Saisir le nombre d'heure de travail à temps complet (100%), " \
+        "sans tenir compte du régime de travail"
 
 # Upload de fichiers
 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)
+    path = "%s/poste/%s/%s" % (
+        instance._meta.app_label, instance.poste_id, filename
+    )
     return path
 
+
 def dossier_piece_dispatch(instance, filename):
-    path = "%s/dossier/%s/%s" % (instance._meta.app_label, instance.dossier_id, filename)
+    path = "%s/dossier/%s/%s" % (
+        instance._meta.app_label, instance.dossier_id, filename
+    )
     return path
 
+
 def employe_piece_dispatch(instance, filename):
-    path = "%s/employe/%s/%s" % (instance._meta.app_label, instance.employe_id, filename)
+    path = "%s/employe/%s/%s" % (
+        instance._meta.app_label, instance.employe_id, filename
+    )
     return path
 
+
 def contrat_dispatch(instance, filename):
-    path = "%s/contrat/%s/%s" % (instance._meta.app_label, instance.dossier_id, filename)
+    path = "%s/contrat/%s/%s" % (
+        instance._meta.app_label, instance.dossier_id, filename
+    )
     return path
 
 
+class DateActiviteMixin(models.Model):
+    """
+    Mixin pour mettre à jour l'activité d'un modèle
+    """
+    class Meta:
+        abstract = True
+    date_creation = models.DateTimeField(auto_now_add=True,
+            null=True, blank=True,
+            verbose_name=u"Date de création",)
+    date_modification = models.DateTimeField(auto_now=True,
+            null=True, blank=True,
+            verbose_name=u"Date de modification",)
+
+
+class Archivable(models.Model):
+    archive = models.BooleanField(u'archivé', default=False)
+
+    objects = ArchivableManager()
+    avec_archives = models.Manager()
+
+    class Meta:
+        abstract = True
+
+
 class DevisableMixin(object):
 
     def get_annee_pour_taux_devise(self):
         return datetime.datetime.now().year
 
-
     def taux_devise(self, devise=None):
         if devise is None:
             devise = self.devise
@@ -71,31 +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")
+    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
@@ -112,8 +165,10 @@ POSTE_APPEL_CHOICES = (
     ('externe', 'Externe'),
 )
 
-class Poste_(AUFMetadata):
-    """Un Poste est un emploi (job) à combler dans une implantation.
+
+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éé.
     Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
     """
@@ -121,114 +176,152 @@ class Poste_(AUFMetadata):
     objects = PosteManager()
 
     # Identification
-    nom = models.CharField(max_length=255,
-                            verbose_name = u"Titre du poste", )
-    nom_feminin = models.CharField(max_length=255,
-                            verbose_name = u"Titre du poste (au féminin)",
-                            null=True)
-    implantation = models.ForeignKey(ref.Implantation, help_text=u"Taper le nom de l'implantation ou sa région", 
-                            db_column='implantation', related_name='+')
-    type_poste = models.ForeignKey('TypePoste', db_column='type_poste', help_text=u"Taper le nom du type de poste",
-                            related_name='+',
-                            null=True,
-                            verbose_name=u"type de poste")
-    service = models.ForeignKey('Service', db_column='service',
-                            related_name='+',
-                            verbose_name = u"direction/service/pôle support",
-                            null=True,)
-    responsable = models.ForeignKey('Poste', db_column='responsable', 
-                            related_name='+',
-                            null=True,
-                            help_text=u"Taper le nom du poste ou du type de poste", 
-                            verbose_name = u"Poste du responsable", )
-                                
+    nom = models.CharField(u"Titre du poste", max_length=255)
+    nom_feminin = models.CharField(
+         u"Titre du poste (au féminin)", max_length=255, null=True
+    )
+    implantation = models.ForeignKey(
+        ref.Implantation,
+        help_text=u"Taper le nom de l'implantation ou sa région",
+        db_column='implantation', related_name='+'
+    )
+    type_poste = models.ForeignKey(
+        'TypePoste', db_column='type_poste',
+        help_text=u"Taper le nom du type de poste", related_name='+',
+        null=True, verbose_name=u"type de poste"
+    )
+    service = models.ForeignKey(
+        'Service', db_column='service', related_name='%(app_label)s_postes',
+        verbose_name=u"direction/service/pôle support", null=True
+    )
+    responsable = models.ForeignKey(
+        'Poste', db_column='responsable',
+        related_name='+', null=True,
+        help_text=u"Taper le nom du poste ou du type de poste",
+        verbose_name=u"Poste du responsable"
+    )
+
     # Contrat
-    regime_travail = models.DecimalField(max_digits=12, decimal_places=2,
-                            default=REGIME_TRAVAIL_DEFAULT, null=True,
-                            verbose_name = u"Temps de travail",
-                            help_text="% du temps complet")
-    regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
-                            decimal_places=2, null=True,
-                            default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
-                            verbose_name= u"Nb. heures par semaine",
-                            help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT)
+    regime_travail = models.DecimalField(
+        u"temps de travail", max_digits=12, decimal_places=2,
+        default=REGIME_TRAVAIL_DEFAULT, null=True,
+        help_text="% du temps complet"
+    )
+    regime_travail_nb_heure_semaine = models.DecimalField(
+        u"nb. heures par semaine", max_digits=12, decimal_places=2,
+        null=True, default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
+        help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
+    )
 
     # Recrutement
-    local = models.NullBooleanField(verbose_name = u"Local", default=True,
-                            null=True, blank=True)
-    expatrie = models.NullBooleanField(verbose_name = u"Expatrié", default=False,
-                            null=True, blank=True)
+    local = models.NullBooleanField(
+        u"local", default=True, null=True, blank=True
+    )
+    expatrie = models.NullBooleanField(
+        u"expatrié", default=False, null=True, blank=True
+    )
     mise_a_disposition = models.NullBooleanField(
-                            verbose_name = u"Mise à disposition",
-                            null=True, default=False)
-    appel = models.CharField(max_length=10, null=True,
-                            verbose_name = u"Appel à candidature",
-                            choices=POSTE_APPEL_CHOICES,
-                            default='interne')
+        u"mise à disposition", null=True, default=False
+    )
+    appel = models.CharField(
+        u"Appel à candidature", max_length=10, null=True,
+        choices=POSTE_APPEL_CHOICES, default='interne'
+    )
 
     # Rémunération
-    classement_min = models.ForeignKey('Classement',
-                            db_column='classement_min', related_name='+',
-                            null=True, blank=True)
-    classement_max = models.ForeignKey('Classement',
-                            db_column='classement_max', related_name='+',
-                            null=True, blank=True)
-    valeur_point_min = models.ForeignKey('ValeurPoint', help_text=u"Taper le code ou le nom de l'implantation",
-                            db_column='valeur_point_min', related_name='+',
-                            null=True, blank=True)
-    valeur_point_max = models.ForeignKey('ValeurPoint', help_text=u"Taper le code ou le nom de l'implantation",
-                            db_column='valeur_point_max', related_name='+',
-                            null=True, blank=True)
-    devise_min = models.ForeignKey('Devise', db_column='devise_min', null=True,
-                            related_name='+',)
-    devise_max = models.ForeignKey('Devise', db_column='devise_max', null=True,
-                            related_name='+',)
-    salaire_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, default=0)
-    salaire_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, default=0)
-    indemn_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, default=0)
-    indemn_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, default=0)
-    autre_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, default=0)
-    autre_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, default=0)
+    classement_min = models.ForeignKey(
+        'Classement', db_column='classement_min', related_name='+',
+        null=True, blank=True
+    )
+    classement_max = models.ForeignKey(
+        'Classement', db_column='classement_max', related_name='+',
+        null=True, blank=True
+    )
+    valeur_point_min = models.ForeignKey(
+        'ValeurPoint',
+        help_text=u"Taper le code ou le nom de l'implantation",
+        db_column='valeur_point_min', related_name='+', null=True,
+        blank=True
+    )
+    valeur_point_max = models.ForeignKey(
+        'ValeurPoint',
+        help_text=u"Taper le code ou le nom de l'implantation",
+        db_column='valeur_point_max', related_name='+', null=True,
+        blank=True
+    )
+    devise_min = models.ForeignKey(
+        'Devise', db_column='devise_min', null=True, related_name='+'
+    )
+    devise_max = models.ForeignKey(
+        'Devise', db_column='devise_max', null=True, related_name='+'
+    )
+    salaire_min = models.DecimalField(
+        max_digits=12, decimal_places=2, default=0,
+    )
+    salaire_max = models.DecimalField(
+        max_digits=12, decimal_places=2, default=0,
+    )
+    indemn_min = models.DecimalField(
+        max_digits=12, decimal_places=2, default=0,
+    )
+    indemn_max = models.DecimalField(
+        max_digits=12, decimal_places=2, default=0,
+    )
+    autre_min = models.DecimalField(
+        max_digits=12, decimal_places=2, default=0,
+    )
+    autre_max = models.DecimalField(
+        max_digits=12, decimal_places=2, default=0,
+    )
 
     # Comparatifs de rémunération
-    devise_comparaison = models.ForeignKey('Devise', null=True, blank=True,
-                            db_column='devise_comparaison',
-                            related_name='+', )
-    comp_locale_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_locale_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_universite_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_universite_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_fonctionpub_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_fonctionpub_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_ong_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_ong_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_autre_min = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
-    comp_autre_max = models.DecimalField(max_digits=12, decimal_places=2,
-                            null=True, blank=True)
+    devise_comparaison = models.ForeignKey(
+        'Devise', null=True, blank=True, db_column='devise_comparaison',
+        related_name='+'
+    )
+    comp_locale_min = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_locale_max = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_universite_min = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_universite_max = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_fonctionpub_min = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_fonctionpub_max = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_ong_min = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_ong_max = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_autre_min = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
+    comp_autre_max = models.DecimalField(
+        max_digits=12, decimal_places=2, null=True, blank=True
+    )
 
     # Justification
     justification = models.TextField(null=True, blank=True)
 
     # Autres Metadata
-    date_debut = models.DateField(verbose_name=u"Date de début", help_text=HELP_TEXT_DATE,
-                            null=True, blank=True)
-    date_fin = models.DateField(verbose_name=u"Date de fin", help_text=HELP_TEXT_DATE,
-                            null=True, blank=True)
+    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
+    )
 
     class Meta:
         abstract = True
@@ -238,23 +331,31 @@ class Poste_(AUFMetadata):
         ordering = ["nom"]
 
     def __unicode__(self):
-        representation = u'%s - %s [%s]' % (self.implantation, self.nom,
-                            self.id)
+        representation = u'%s - %s [%s]' % (
+            self.implantation, self.nom, self.id
+        )
         return representation
 
+    prefix_implantation = "implantation__zone_administrative"
 
-    prefix_implantation = "implantation__region"
-    def get_regions(self):
-        return [self.implantation.region]
+    def get_zones_administratives(self):
+        return [self.implantation.zone_administrative]
 
     def get_devise(self):
-        return ValeurPoint.objects.filter(implantation=self.implantation, devise__archive=False).order_by('annee')[0].devise
+        vp = ValeurPoint.objects.filter(
+            implantation=self.implantation, devise__archive=False
+        ).order_by('annee')
+        if len(vp) > 0:
+            return vp[0].devise
+        else:
+            return Devise.objects.get(code='EUR')
+
 
 class Poste(Poste_):
     __doc__ = Poste_.__doc__
 
     # meta dématérialisation :  pour permettre le filtrage
-    vacant = models.NullBooleanField(verbose_name = u"vacant", null=True, blank=True)
+    vacant = models.NullBooleanField(u"vacant", null=True, blank=True)
 
     def is_vacant(self):
         vacant = True
@@ -263,12 +364,21 @@ class Poste(Poste_):
         return vacant
 
     def occupe_par(self):
-        """Retourne la liste d'employé occupant ce poste.
+        """
+        Retourne la liste d'employé occupant ce poste.
         Généralement, retourne une liste d'un élément.
         Si poste inoccupé, retourne liste vide.
         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())]
+        return [
+            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 = (
@@ -279,15 +389,18 @@ POSTE_FINANCEMENT_CHOICES = (
 
 
 class PosteFinancement_(models.Model):
-    """Pour un Poste, structure d'informations décrivant comment on prévoit
+    """
+    Pour un Poste, structure d'informations décrivant comment on prévoit
     financer ce Poste.
     """
-    poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='%(app_label)s_financements')
     type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
-    pourcentage = models.DecimalField(max_digits=12, decimal_places=2,
-            help_text="ex.: 33.33 % (décimale avec point)")
+    pourcentage = models.DecimalField(
+        max_digits=12, decimal_places=2,
+        help_text="ex.: 33.33 % (décimale avec point)"
+    )
     commentaire = models.TextField(
-            help_text="Spécifiez la source de financement.")
+        help_text="Spécifiez la source de financement."
+    )
 
     class Meta:
         abstract = True
@@ -301,18 +414,23 @@ class PosteFinancement_(models.Model):
 
 
 class PosteFinancement(PosteFinancement_):
-    pass
+    poste = models.ForeignKey(
+        Poste, db_column='poste', related_name='rh_financements'
+    )
+
+reversion.register(PosteFinancement, format='xml')
 
 
 class PostePiece_(models.Model):
-    """Documents relatifs au Poste.
+    """
+    Documents relatifs au Poste.
     Ex.: Description de poste
     """
-    poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='%(app_label)s_pieces')
-    nom = models.CharField(verbose_name = u"Nom", max_length=255)
-    fichier = models.FileField(verbose_name = u"Fichier",
-                            upload_to=poste_piece_dispatch,
-                            storage=storage_prive)
+    nom = models.CharField(u"Nom", max_length=255)
+    fichier = models.FileField(
+        u"Fichier", upload_to=poste_piece_dispatch, storage=storage_prive,
+        max_length=255
+    )
 
     class Meta:
         abstract = True
@@ -321,101 +439,135 @@ class PostePiece_(models.Model):
     def __unicode__(self):
         return u'%s' % (self.nom)
 
+
 class PostePiece(PostePiece_):
-    pass
+    poste = models.ForeignKey(
+        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.
+    De la même manière qu'un dossier, un poste peut-être comparé à un autre
+    poste.
     """
-    poste = models.ForeignKey('%s.Poste' % app_context(), related_name='%(app_label)s_comparaisons_internes')
     objects = PosteComparaisonManager()
 
-    implantation = models.ForeignKey(ref.Implantation, null=True, blank=True, related_name="+")
-    nom = models.CharField(verbose_name = u"Poste", max_length=255, null=True, blank=True)
+    implantation = models.ForeignKey(
+        ref.Implantation, null=True, blank=True, related_name="+"
+    )
+    nom = models.CharField(u"Poste", max_length=255, null=True, blank=True)
     montant = models.IntegerField(null=True)
-    devise = models.ForeignKey("Devise", related_name='+', null=True, blank=True)
+    devise = models.ForeignKey(
+        "Devise", related_name='+', null=True, blank=True
+    )
 
     class Meta:
         abstract = True
 
-
     def __unicode__(self):
         return self.nom
 
+
 class PosteComparaison(PosteComparaison_):
-    objects = NoDeleteManager()
+    poste = models.ForeignKey(
+        Poste, related_name='rh_comparaisons_internes'
+    )
 
-class PosteCommentaire_(Commentaire):
-    poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='+')
+reversion.register(PosteComparaison, format='xml')
 
-    class Meta:
-        abstract = True
 
-class PosteCommentaire(PosteCommentaire_):
-    pass
+class PosteCommentaire(Commentaire):
+    poste = models.ForeignKey(
+        Poste, db_column='poste', related_name='commentaires'
+    )
+
+reversion.register(PosteCommentaire, format='xml')
 
 ### EMPLOYÉ/PERSONNE
 
-class Employe(AUFMetadata):
-    """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
+class Employe(models.Model):
+    """
+    Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
     Dossiers qu'il occupe ou a occupé de Postes.
 
     Cette classe aurait pu avantageusement s'appeler Personne car la notion
     d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
     """
+
+    objects = EmployeManager()
+
     # Identification
     nom = models.CharField(max_length=255)
-    prenom = models.CharField(max_length=255, verbose_name = u"Prénom")
-    nom_affichage = models.CharField(max_length=255,
-                            verbose_name = u"Nom d'affichage",
-                            null=True, blank=True)
-    nationalite = models.ForeignKey(ref.Pays, to_field='code',
-                            db_column='nationalite',
-                            related_name='employes_nationalite',
-                            verbose_name = u"Nationalité",
-                            blank=True, null=True)
-    date_naissance = models.DateField(verbose_name = u"Date de naissance",
-                            help_text=HELP_TEXT_DATE,
-                            validators=[validate_date_passee],
-                            null=True, blank=True)
+    prenom = models.CharField(u"prénom", max_length=255)
+    nom_affichage = models.CharField(
+        u"nom d'affichage", max_length=255, null=True, blank=True
+    )
+    nationalite = models.ForeignKey(
+        ref.Pays, to_field='code', db_column='nationalite',
+        related_name='employes_nationalite', verbose_name=u"nationalité",
+        blank=True, null=True
+    )
+    date_naissance = models.DateField(
+        u"date de naissance", help_text=HELP_TEXT_DATE,
+        validators=[validate_date_passee], null=True, blank=True
+    )
     genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
 
     # Infos personnelles
-    situation_famille = models.CharField(max_length=1,
-                            choices=SITUATION_CHOICES,
-                            verbose_name = u"Situation familiale",
-                            null=True, blank=True)
-    date_entree = models.DateField(verbose_name = u"Date d'entrée à l'AUF",
-                            help_text=HELP_TEXT_DATE,
-                            null=True, blank=True)
+    situation_famille = models.CharField(
+        u"situation familiale", max_length=1, choices=SITUATION_CHOICES,
+        null=True, blank=True
+    )
+    date_entree = models.DateField(
+        u"date d'entrée à l'AUF", help_text=HELP_TEXT_DATE, null=True,
+        blank=True
+    )
 
     # Coordonnées
-    tel_domicile = models.CharField(max_length=255,
-                            verbose_name = u"Tél. domicile",
-                            null=True, blank=True)
-    tel_cellulaire = models.CharField(max_length=255,
-                            verbose_name = u"Tél. cellulaire",
-                            null=True, blank=True)
+    tel_domicile = models.CharField(
+        u"tél. domicile", max_length=255, null=True, blank=True
+    )
+    tel_cellulaire = models.CharField(
+        u"tél. cellulaire", max_length=255, null=True, blank=True
+    )
     adresse = models.CharField(max_length=255, null=True, blank=True)
     ville = models.CharField(max_length=255, null=True, blank=True)
     province = models.CharField(max_length=255, null=True, blank=True)
     code_postal = models.CharField(max_length=255, null=True, blank=True)
-    pays = models.ForeignKey(ref.Pays, to_field='code', db_column='pays',
-                            related_name='employes',
-                            null=True, blank=True)
+    pays = models.ForeignKey(
+        ref.Pays, to_field='code', db_column='pays',
+        related_name='employes', null=True, blank=True
+    )
+    courriel_perso = models.EmailField(
+        u'adresse courriel personnelle', blank=True
+    )
 
     # meta dématérialisation :  pour permettre le filtrage
-    nb_postes = models.IntegerField(verbose_name = u"nombre de postes", null=True, blank=True)
+    nb_postes = models.IntegerField(u"nombre de postes", null=True, blank=True)
 
     class Meta:
-        ordering = ['nom','prenom']
+        ordering = ['nom', 'prenom']
         verbose_name = u"Employé"
         verbose_name_plural = u"Employés"
 
     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':
@@ -425,33 +577,79 @@ class Employe(AUFMetadata):
         return civilite
 
     def url_photo(self):
-        """Retourne l'URL du service retournant la photo de l'Employe.
+        """
+        Retourne l'URL du service retournant la photo de l'Employe.
         Équivalent reverse url 'rh_photo' avec id en param.
         """
         from django.core.urlresolvers import reverse
-        return reverse('rh_photo', kwargs={'id':self.id})
+        return reverse('rh_photo', kwargs={'id': self.id})
 
     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():
@@ -464,6 +662,7 @@ class Employe(AUFMetadata):
         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
@@ -471,23 +670,34 @@ class Employe(AUFMetadata):
             pass
         return poste
 
-    prefix_implantation = "rh_dossiers__poste__implantation__region"
-    def get_regions(self):
-        regions = []
-        for d in self.dossiers.all():
-            regions.append(d.poste.implantation.region)
-        return regions
+    prefix_implantation = \
+            "rh_dossiers__poste__implantation__zone_administrative"
+
+    def get_zones_administratives(self):
+        return [
+            d.poste.implantation.zone_administrative
+            for d in self.dossiers.all()
+        ]
+
+reversion.register(Employe, format='xml', follow=[
+    'pieces', 'commentaires', 'ayantdroits'
+])
 
 
 class EmployePiece(models.Model):
-    """Documents relatifs à un employé.
+    """
+    Documents relatifs à un employé.
     Ex.: CV...
     """
-    employe = models.ForeignKey('Employe', db_column='employe')
-    nom = models.CharField(verbose_name="Nom", max_length=255)
-    fichier = models.FileField(verbose_name="Fichier",
-                            upload_to=employe_piece_dispatch,
-                            storage=storage_prive)
+    employe = models.ForeignKey(
+        'Employe', db_column='employe', related_name="pieces",
+        verbose_name=u"employé"
+    )
+    nom = models.CharField(max_length=255)
+    fichier = models.FileField(
+        u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive,
+        max_length=255
+    )
 
     class Meta:
         ordering = ['nom']
@@ -497,14 +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 = models.ForeignKey(
+        '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'),
@@ -513,35 +729,37 @@ LIEN_PARENTE_CHOICES = (
     ('Fils', 'Fils'),
 )
 
-class AyantDroit(AUFMetadata):
-    """Personne en relation avec un Employe.
+
+class AyantDroit(models.Model):
+    """
+    Personne en relation avec un Employe.
     """
     # Identification
     nom = models.CharField(max_length=255)
-    prenom = models.CharField(max_length=255,
-                            verbose_name = u"Prénom",)
-    nom_affichage = models.CharField(max_length=255,
-                            verbose_name = u"Nom d'affichage",
-                            null=True, blank=True)
-    nationalite = models.ForeignKey(ref.Pays, to_field='code',
-                            db_column='nationalite',
-                            related_name='ayantdroits_nationalite',
-                            verbose_name = u"Nationalité",
-                            null=True, blank=True)
-    date_naissance = models.DateField(verbose_name = u"Date de naissance",
-                            help_text=HELP_TEXT_DATE,
-                            validators=[validate_date_passee],
-                            null=True, blank=True)
+    prenom = models.CharField(u"prénom", max_length=255)
+    nom_affichage = models.CharField(
+        u"nom d'affichage", max_length=255, null=True, blank=True
+    )
+    nationalite = models.ForeignKey(
+        ref.Pays, to_field='code', db_column='nationalite',
+        related_name='ayantdroits_nationalite',
+        verbose_name=u"nationalité", null=True, blank=True
+    )
+    date_naissance = models.DateField(
+        u"Date de naissance", help_text=HELP_TEXT_DATE,
+        validators=[validate_date_passee], null=True, blank=True
+    )
     genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
 
     # Relation
-    employe = models.ForeignKey('Employe', db_column='employe',
-                            related_name='ayantdroits',
-                            verbose_name = u"Employé")
-    lien_parente = models.CharField(max_length=10,
-                            choices=LIEN_PARENTE_CHOICES,
-                            verbose_name = u"Lien de parenté",
-                            null=True, blank=True)
+    employe = models.ForeignKey(
+        'Employe', db_column='employe', related_name='ayantdroits',
+        verbose_name=u"Employé"
+    )
+    lien_parente = models.CharField(
+        u"lien de parenté", max_length=10, choices=LIEN_PARENTE_CHOICES,
+        null=True, blank=True
+    )
 
     class Meta:
         ordering = ['nom', ]
@@ -549,19 +767,26 @@ class AyantDroit(AUFMetadata):
         verbose_name_plural = u"Ayants droit"
 
     def __unicode__(self):
-        return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
+        return u'%s %s' % (self.nom.upper(), self.prenom, )
+
+    prefix_implantation = \
+            "employe__dossiers__poste__implantation__zone_administrative"
 
-    prefix_implantation = "employe__dossiers__poste__implantation__region"
-    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'])
 
 
 class AyantDroitCommentaire(Commentaire):
-    ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit',
-                            related_name='+')
+    ayant_droit = models.ForeignKey(
+        'AyantDroit', db_column='ayant_droit', related_name='commentaires'
+    )
+
+reversion.register(AyantDroitCommentaire, format='xml')
 
 
 ### DOSSIER
@@ -577,8 +802,10 @@ COMPTE_COMPTA_CHOICES = (
     ('aucun', 'Aucun'),
 )
 
-class Dossier_(AUFMetadata, DevisableMixin):
-    """Le Dossier regroupe les informations relatives à l'occupation
+
+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é
     par un Employe.
 
@@ -591,48 +818,62 @@ class Dossier_(AUFMetadata, DevisableMixin):
 
     # TODO: OneToOne ??
     statut = models.ForeignKey('Statut', related_name='+', null=True)
-    organisme_bstg = models.ForeignKey('OrganismeBstg',
-                            db_column='organisme_bstg',
-                            related_name='+',
-                            verbose_name = u"Organisme",
-                            help_text="Si détaché (DET) ou \
-                                    mis à disposition (MAD), \
-                                    préciser l'organisme.",
-                            null=True, blank=True)
+    organisme_bstg = models.ForeignKey(
+        'OrganismeBstg', db_column='organisme_bstg', related_name='+',
+        verbose_name=u"organisme",
+        help_text=(
+            u"Si détaché (DET) ou mis à disposition (MAD), "
+            u"préciser l'organisme."
+        ), null=True, blank=True
+    )
 
     # Recrutement
     remplacement = models.BooleanField(default=False)
-    remplacement_de = models.ForeignKey('self', related_name='+',
-                            help_text=u"Taper le nom de l'employé",
-                            null=True, blank=True)
-    statut_residence = models.CharField(max_length=10, default='local',
-                            verbose_name = u"Statut", null=True,
-                            choices=STATUT_RESIDENCE_CHOICES)
+    remplacement_de = models.ForeignKey(
+        'self', related_name='+', help_text=u"Taper le nom de l'employé",
+        null=True, blank=True
+    )
+    statut_residence = models.CharField(
+        u"statut", max_length=10, default='local', null=True,
+        choices=STATUT_RESIDENCE_CHOICES
+    )
 
     # Rémunération
-    classement = models.ForeignKey('Classement', db_column='classement',
-                            related_name='+',
-                            null=True, blank=True)
-    regime_travail = models.DecimalField(max_digits=12, null=True,
-                            decimal_places=2,
-                            default=REGIME_TRAVAIL_DEFAULT,
-                            verbose_name = u"Régime de travail",
-                            help_text="% du temps complet")
-    regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12,
-                            decimal_places=2, null=True,
-                            default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
-                            verbose_name=u"Nb. heures par semaine",
-                            help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT)
+    classement = models.ForeignKey(
+        'Classement', db_column='classement', related_name='+', null=True,
+        blank=True
+    )
+    regime_travail = models.DecimalField(
+        u"régime de travail", max_digits=12, null=True, decimal_places=2,
+        default=REGIME_TRAVAIL_DEFAULT, help_text="% du temps complet"
+    )
+    regime_travail_nb_heure_semaine = models.DecimalField(
+        u"nb. heures par semaine", max_digits=12,
+        decimal_places=2, null=True,
+        default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
+        help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
+    )
 
     # Occupation du Poste par cet Employe (anciennement "mandat")
-    date_debut = models.DateField(verbose_name = u"Date de début d'occupation \
-                            de poste",)
-    date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \
-                            de poste",
-                            null=True, blank=True)
+    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,
+        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
@@ -648,7 +889,7 @@ class Dossier_(AUFMetadata, DevisableMixin):
 
         montant = coeff * point.valeur
         devise = point.devise
-        return {'montant':montant, 'devise':devise}
+        return {'montant': montant, 'devise': devise}
 
     def __unicode__(self):
         poste = self.poste.nom
@@ -656,10 +897,10 @@ class Dossier_(AUFMetadata, DevisableMixin):
             poste = self.poste.nom_feminin
         return u'%s - %s' % (self.employe, poste)
 
-    prefix_implantation = "poste__implantation__region"
-    def get_regions(self):
-        return [self.poste.implantation.region]
+    prefix_implantation = "poste__implantation__zone_administrative"
 
+    def get_zones_administratives(self):
+        return [self.poste.implantation.zone_administrative]
 
     def remunerations(self):
         key = "%s_remunerations" % self._meta.app_label
@@ -672,7 +913,8 @@ class Dossier_(AUFMetadata, DevisableMixin):
 
     def get_salaire(self):
         try:
-            return [r for r in self.remunerations().order_by('-date_debut') if r.type_id == 1][0]
+            return [r for r in self.remunerations().order_by('-date_debut')
+                    if r.type_id == 1][0]
         except:
             return None
 
@@ -700,28 +942,32 @@ class Dossier_(AUFMetadata, DevisableMixin):
         18  Prime de 13ième mois
         19  Prime d'intérim
         """
-        ids = [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
-        return [r for r in self.remunerations_en_cours().all() if r.type_id in ids]
+        ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
+        return [r for r in self.remunerations_en_cours().all()
+                if r.type_id in ids]
 
     def get_charges_salariales(self):
         """
         20 Charges salariales ?
         """
-        ids = [20, ]
-        return [r for r in self.remunerations_en_cours().all() if r.type_id in ids]
+        ids = [20]
+        return [r for r in self.remunerations_en_cours().all()
+                if r.type_id in ids]
 
     def get_charges_patronales(self):
         """
         17  Charges patronales
         """
-        ids = [17, ]
-        return [r for r in self.remunerations_en_cours().all() if r.type_id in ids]
+        ids = [17]
+        return [r for r in self.remunerations_en_cours().all()
+                if r.type_id in ids]
 
     def get_remunerations_tierces(self):
         """
         2   Salaire MAD
         """
-        return [r for r in self.remunerations_en_cours().all() if r.type_id in (2, )]
+        return [r for r in self.remunerations_en_cours().all()
+                if r.type_id in (2,)]
 
     # DEVISE LOCALE
 
@@ -835,31 +1081,185 @@ class Dossier_(AUFMetadata, 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) \
+                and (self.date_fin is None or self.date_fin >= today) \
+                and not (self.date_fin is None and self.date_debut is None)
+
 
 class Dossier(Dossier_):
     __doc__ = Dossier_.__doc__
-    poste = models.ForeignKey('%s.Poste' % app_context(),
-        db_column='poste',
-        related_name='%(app_label)s_dossiers',
+    poste = models.ForeignKey(
+        Poste, db_column='poste', related_name='rh_dossiers',
         help_text=u"Taper le nom du poste ou du type de poste",
+    )
+    employe = models.ForeignKey(
+        'Employe', db_column='employe',
+        help_text=u"Taper le nom de l'employé",
+        related_name='rh_dossiers', verbose_name=u"employé"
+    )
+    principal = models.BooleanField(
+        u"dossier principal", default=True,
+        help_text=(
+            u"Ce dossier est pour le principal poste occupé par l'employé"
         )
-    employe = models.ForeignKey('Employe', db_column='employe',
-                            help_text=u"Taper le nom de l'employé",
-                            related_name='%(app_label)s_dossiers',
-                            verbose_name=u"Employé")
-    principal = models.BooleanField(verbose_name=u"Principal?", default=True,
-            help_text=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):
-    """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
+    """
+    Documents relatifs au Dossier (à l'occupation de ce poste par employé).
     Ex.: Lettre de motivation.
     """
-    dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_dossierpieces')
-    nom = models.CharField(verbose_name = u"Nom", max_length=255)
-    fichier = models.FileField(verbose_name = u"Fichier",
-                            upload_to=dossier_piece_dispatch,
-                            storage=storage_prive)
+    nom = models.CharField(max_length=255)
+    fichier = models.FileField(
+        upload_to=dossier_piece_dispatch, storage=storage_prive,
+        max_length=255
+    )
 
     class Meta:
         abstract = True
@@ -868,29 +1268,37 @@ class DossierPiece_(models.Model):
     def __unicode__(self):
         return u'%s' % (self.nom)
 
+
 class DossierPiece(DossierPiece_):
-    pass
+    dossier = models.ForeignKey(
+        Dossier, db_column='dossier', related_name='rh_dossierpieces'
+    )
 
-class DossierCommentaire_(Commentaire):
-    dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+')
-    class Meta:
-        abstract = True
+reversion.register(DossierPiece, format='xml')
+
+class DossierCommentaire(Commentaire):
+    dossier = models.ForeignKey(
+        Dossier, db_column='dossier', related_name='commentaires'
+    )
+
+reversion.register(DossierCommentaire, format='xml')
 
-class DossierCommentaire(DossierCommentaire_):
-    pass
 
 class DossierComparaison_(models.Model, DevisableMixin):
     """
     Photo d'une comparaison salariale au moment de l'embauche.
     """
-    dossier = models.ForeignKey('%s.Dossier' % app_context(), related_name='%(app_label)s_comparaisons')
     objects = DossierComparaisonManager()
 
-    implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True)
+    implantation = models.ForeignKey(
+        ref.Implantation, related_name="+", null=True, blank=True
+    )
     poste = models.CharField(max_length=255, null=True, blank=True)
     personne = models.CharField(max_length=255, null=True, blank=True)
     montant = models.IntegerField(null=True)
-    devise = models.ForeignKey('Devise', related_name='+', null=True, blank=True)
+    devise = models.ForeignKey(
+        'Devise', related_name='+', null=True, blank=True
+    )
 
     class Meta:
         abstract = True
@@ -900,32 +1308,42 @@ class DossierComparaison_(models.Model, DevisableMixin):
 
 
 class DossierComparaison(DossierComparaison_):
-    pass
+    dossier = models.ForeignKey(
+        Dossier, related_name='rh_comparaisons'
+    )
+
+reversion.register(DossierComparaison, format='xml')
+
 
 ### RÉMUNÉRATION
 
-class RemunerationMixin(AUFMetadata):
-    dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_remunerations')
+class RemunerationMixin(models.Model):
+
     # Identification
-    type = models.ForeignKey('TypeRemuneration', db_column='type', 
-                            related_name='+',
-                            verbose_name = u"Type de rémunération")
-    type_revalorisation = models.ForeignKey('TypeRevalorisation',
-                            db_column='type_revalorisation',
-                            related_name='+',
-                            verbose_name = u"Type de revalorisation",
-                            null=True, blank=True)
-    montant = models.DecimalField(null=True, blank=True,
-                            default=0, max_digits=12, decimal_places=2)
-                            # Annuel (12 mois, 52 semaines, 364 jours?)
-    devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
+    type = models.ForeignKey(
+        'TypeRemuneration', db_column='type', related_name='+',
+        verbose_name=u"type de rémunération"
+    )
+    type_revalorisation = models.ForeignKey(
+        'TypeRevalorisation', db_column='type_revalorisation',
+        related_name='+', verbose_name=u"type de revalorisation",
+        null=True, blank=True
+    )
+    montant = models.DecimalField(
+        null=True, blank=True, max_digits=12, decimal_places=2
+    )  # Annuel (12 mois, 52 semaines, 364 jours?)
+    devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
+
     # commentaire = precision
     commentaire = models.CharField(max_length=255, null=True, blank=True)
+
     # date_debut = anciennement date_effectif
-    date_debut = models.DateField(verbose_name = u"Date de début",
-                            null=True, blank=True)
-    date_fin = models.DateField(verbose_name = 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
@@ -934,17 +1352,78 @@ class RemunerationMixin(AUFMetadata):
     def __unicode__(self):
         return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
 
+
 class Remuneration_(RemunerationMixin, DevisableMixin):
-    """Structure de rémunération (données budgétaires) en situation normale
+    """
+    Structure de rémunération (données budgétaires) en situation normale
     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)
 
     def montant_avec_regime(self):
-        return round(self.montant * (self.dossier.regime_travail/100), 2)
+        return round(self.montant * (self.dossier.regime_travail / 100), 2)
 
     def montant_euro_mois(self):
         return round(self.montant_euros() / 12, 2)
@@ -963,33 +1442,36 @@ class Remuneration_(RemunerationMixin, DevisableMixin):
 
 
 class Remuneration(Remuneration_):
-    pass
+    dossier = models.ForeignKey(
+        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):
-    """Document juridique qui encadre la relation de travail d'un Employe
+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
     relation de travail) plusieurs contrats peuvent être associés.
     """
     objects = ContratManager()
-    dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_contrats')
-    type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat', 
-                            related_name='+',
-                            verbose_name = u"type de contrat")
-    date_debut = models.DateField(verbose_name = u"Date de début")
-    date_fin = models.DateField(verbose_name = u"Date de fin",
-                            null=True, blank=True)
-    fichier = models.FileField(verbose_name = u"Fichier",
-                            upload_to=contrat_dispatch,
-                            storage=storage_prive,
-                            null=True, blank=True)
+    type_contrat = models.ForeignKey(
+        'TypeContrat', db_column='type_contrat',
+        verbose_name=u'type de contrat', related_name='+'
+    )
+    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, max_length=255
+    )
 
     class Meta:
         abstract = True
@@ -1000,104 +1482,69 @@ class Contrat_(AUFMetadata):
     def __unicode__(self):
         return u'%s - %s' % (self.dossier, self.id)
 
+
 class Contrat(Contrat_):
-    pass
-        
+    dossier = models.ForeignKey(
+        Dossier, db_column='dossier', related_name='rh_contrats'
+    )
 
-### ÉVÉNEMENTS
-
-#class Evenement_(AUFMetadata):
-#    """Un Evenement sert à déclarer une situation temporaire (exceptionnelle) 
-#    d'un Dossier qui vient altérer des informations normales liées à un Dossier 
-#    (ex.: la Remuneration).
-#    
-#    Ex.: congé de maternité, maladie...
-#    
-#    Lors de ces situations exceptionnelles, l'Employe a un régime de travail
-#    différent et une rémunération en conséquence. On souhaite toutefois
-#    conserver le Dossier intact afin d'éviter une re-saisie des données lors
-#    du retour à la normale.
-#    """
-#    dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', 
-#                            related_name='+')
-#    nom = models.CharField(max_length=255)
-#    date_debut = models.DateField(verbose_name = u"Date de début")
-#    date_fin = models.DateField(verbose_name = u"Date de fin",
-#                            null=True, blank=True)
-#
-#    class Meta:
-#        abstract = True
-#        ordering = ['nom']
-#        verbose_name = u"Évènement"
-#        verbose_name_plural = u"Évènements"
-#                            
-#    def __unicode__(self):
-#        return u'%s' % (self.nom)
-#
-#
-#class Evenement(Evenement_):
-#    __doc__ = Evenement_.__doc__
-#
-#    
-#class EvenementRemuneration_(RemunerationMixin):
-#    """Structure de rémunération liée à un Evenement qui remplace 
-#    temporairement la Remuneration normale d'un Dossier, pour toute la durée
-#    de l'Evenement.
-#    """
-#    evenement = models.ForeignKey("Evenement", db_column='evenement',
-#                            related_name='+',
-#                            verbose_name = u"Évènement")
-#    # TODO : le champ dossier hérité de Remuneration doit être dérivé
-#    # de l'Evenement associé
-#
-#    class Meta:
-#        abstract = True
-#        ordering = ['evenement', 'type__nom', '-date_fin']
-#        verbose_name = u"Évènement - rémunération"
-#        verbose_name_plural = u"Évènements - rémunérations"
-#
-#
-#class EvenementRemuneration(EvenementRemuneration_):
-#    __doc__ = EvenementRemuneration_.__doc__
-#
-#    class Meta:
-#        abstract = True
-#
-#
-#class EvenementRemuneration(EvenementRemuneration_):
-#    __doc__ = EvenementRemuneration_.__doc__
-# TODO? class ContratPiece(models.Model):
+reversion.register(Contrat, format='xml')
 
 
 ### RÉFÉRENCES RH
 
-class FamilleEmploi(AUFMetadata):
-    """Catégorie utilisée dans la gestion des Postes.
+class CategorieEmploi(models.Model):
+    """
+    Catégorie utilisée dans la gestion des Postes.
     Catégorie supérieure à TypePoste.
     """
     nom = models.CharField(max_length=255)
 
     class Meta:
-        ordering = ['nom']
-        verbose_name = u"Famille d'emploi"
-        verbose_name_plural = u"Familles d'emploi"
+        ordering = ('nom',)
+        verbose_name = u"catégorie d'emploi"
+        verbose_name_plural = u"catégories d'emploi"
 
     def __unicode__(self):
-        return u'%s' % (self.nom)
+        return self.nom
+
+reversion.register(CategorieEmploi, format='xml')
 
-class TypePoste(AUFMetadata):
-    """Catégorie de Poste.
+
+class FamilleProfessionnelle(models.Model):
     """
-    nom = models.CharField(max_length=255)
-    nom_feminin = models.CharField(max_length=255,
-                            verbose_name = u"Nom féminin")
+    Famille professionnelle d'un poste.
+    """
+    nom = models.CharField(max_length=100)
+
+    class Meta:
+        ordering = ('nom',)
+        verbose_name = u'famille professionnelle'
+        verbose_name_plural = u'familles professionnelles'
+
+    def __unicode__(self):
+        return self.nom
+
+reversion.register(FamilleProfessionnelle, format='xml')
 
-    is_responsable = models.BooleanField(default=False,
-                            verbose_name = u"Poste de responsabilité")
-    famille_emploi = models.ForeignKey('FamilleEmploi',
-                            db_column='famille_emploi',
-                            related_name='+',
-                            verbose_name = u"famille d'emploi")
+
+class TypePoste(Archivable):
+    """
+    Catégorie de Poste.
+    """
+    nom = models.CharField(max_length=255)
+    nom_feminin = models.CharField(u"nom féminin", max_length=255)
+    is_responsable = models.BooleanField(
+        u"poste de responsabilité", default=False
+    )
+    categorie_emploi = models.ForeignKey(
+        CategorieEmploi, db_column='categorie_emploi', related_name='+',
+        verbose_name=u"catégorie d'emploi"
+    )
+    famille_professionnelle = models.ForeignKey(
+        FamilleProfessionnelle, related_name='types_de_poste',
+        verbose_name=u"famille professionnelle", blank=True, null=True
+    )
 
     class Meta:
         ordering = ['nom']
@@ -1107,6 +1554,8 @@ 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'),
@@ -1114,26 +1563,31 @@ 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'),
 )
 
-class TypeRemuneration(AUFMetadata):
-    """Catégorie de Remuneration.
+
+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(max_length=30,
-                            choices=TYPE_PAIEMENT_CHOICES,
-                            verbose_name = u"Type de paiement")
-    nature_remuneration = models.CharField(max_length=30,
-                            choices=NATURE_REMUNERATION_CHOICES,
-                            verbose_name = u"Nature de la rémunération")
-    archive = models.BooleanField(verbose_name=u"Archivé", default=False)
+    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
+    )
 
     class Meta:
         ordering = ['nom']
@@ -1141,14 +1595,14 @@ 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):
-    """Justification du changement de la Remuneration.
+
+class TypeRevalorisation(Archivable):
+    """
+    Justification du changement de la Remuneration.
     (Actuellement utilisé dans aucun traitement informatique.)
     """
     nom = models.CharField(max_length=255)
@@ -1161,25 +1615,24 @@ class TypeRevalorisation(AUFMetadata):
     def __unicode__(self):
         return u'%s' % (self.nom)
 
-class Service(AUFMetadata):
-    """Unité administrative où les Postes sont rattachés.
-    """
-    objects = ServiceManager()
+reversion.register(TypeRevalorisation, format='xml')
+
 
-    archive = models.BooleanField(verbose_name=u"Archivé", default=False)
+class Service(Archivable):
+    """
+    Unité administrative où les Postes sont rattachés.
+    """
     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 = (
@@ -1187,8 +1640,10 @@ TYPE_ORGANISME_CHOICES = (
     ('DET', 'Détachement'),
 )
 
-class OrganismeBstg(AUFMetadata):
-    """Organisation d'où provient un Employe mis à disposition (MAD) de
+
+class OrganismeBstg(models.Model):
+    """
+    Organisation d'où provient un Employe mis à disposition (MAD) de
     ou détaché (DET) à l'AUF à titre gratuit.
 
     (BSTG = bien et service à titre gratuit.)
@@ -1208,16 +1663,23 @@ 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):
-    """Statut de l'Employe dans le cadre d'un Dossier particulier.
+class Statut(Archivable):
+    """
+    Statut de l'Employe dans le cadre d'un Dossier particulier.
     """
     # Identification
-    code = models.CharField(max_length=25, unique=True, help_text="Saisir un code court mais lisible pour ce statut : le code est utilisé pour associer les statuts aux autres données tout en demeurant plus lisible qu'un identifiant numérique.")
+    code = models.CharField(
+        max_length=25, unique=True,
+        help_text=(
+            u"Saisir un code court mais lisible pour ce statut : "
+            u"le code est utilisé pour associer les statuts aux autres "
+            u"données tout en demeurant plus lisible qu'un identifiant "
+            u"numérique."
+        )
+    )
     nom = models.CharField(max_length=255)
 
     class Meta:
@@ -1228,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'),
@@ -1239,19 +1703,28 @@ TYPE_CLASSEMENT_CHOICES = (
     ('HG', 'HG - Hors grille [direction]'),
 )
 
+
 class ClassementManager(models.Manager):
     """
     Ordonner les spcéfiquement les classements.
     """
     def get_query_set(self):
-        qs = super(self.__class__, self).get_query_set()
-        qs = qs.extra(select={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
+        qs = super(ClassementManager, self).get_query_set()
+        qs = qs.extra(select={
+            'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
+        })
         qs = qs.extra(order_by=('ponderation', 'echelon',  'degre', ))
         return qs.all()
 
 
-class Classement_(AUFMetadata):
-    """Éléments de classement de la
+class ClassementArchivableManager(ClassementManager,
+                                  ArchivableManager):
+    pass
+
+
+class Classement_(Archivable):
+    """
+    Éléments de classement de la
     "Grille générique de classement hiérarchique".
 
     Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
@@ -1260,44 +1733,50 @@ 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(verbose_name=u"Échelon", blank=True, default=0)
-    degre = models.IntegerField(verbose_name=u"Degré", blank=True, default=0)
-    coefficient = models.FloatField(default=0, verbose_name=u"Coefficient",
-                                    null=True)
+    echelon = models.IntegerField(u"échelon", blank=True, default=0)
+    degre = models.IntegerField(u"degré", blank=True, default=0)
+    coefficient = models.FloatField(u"coefficient", blank=True, null=True)
+
     # Méta
     # annee # au lieu de date_debut et date_fin
     commentaire = models.TextField(null=True, blank=True)
 
     class Meta:
         abstract = True
-        ordering = ['type','echelon','degre','coefficient']
+        ordering = ['type', 'echelon', 'degre', 'coefficient']
         verbose_name = u"Classement"
         verbose_name_plural = u"Classements"
 
     def __unicode__(self):
         return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
 
+
 class Classement(Classement_):
     __doc__ = Classement_.__doc__
 
+reversion.register(Classement, format='xml')
+
 
-class TauxChange_(AUFMetadata):
-    """Taux de change de la devise vers l'euro (EUR)
+class TauxChange_(models.Model):
+    """
+    Taux de change de la devise vers l'euro (EUR)
     pour chaque année budgétaire.
     """
     # Identification
     devise = models.ForeignKey('Devise', db_column='devise')
-    annee = models.IntegerField(verbose_name = u"Année")
-    taux = models.FloatField(verbose_name = u"Taux vers l'euro")
+    annee = models.IntegerField(u"année")
+    taux = models.FloatField(u"taux vers l'euro")
 
     class Meta:
         abstract = True
         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)
@@ -1306,20 +1785,26 @@ class TauxChange_(AUFMetadata):
 class TauxChange(TauxChange_):
     __doc__ = TauxChange_.__doc__
 
-class ValeurPointManager(NoDeleteManager):
+reversion.register(TauxChange, format='xml')
+
+
+class ValeurPointManager(models.Manager):
 
     def get_query_set(self):
-        return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation')
+        return super(ValeurPointManager, self).get_query_set() \
+            .select_related('devise', 'implantation')
 
 
-class ValeurPoint_(AUFMetadata):
-    """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
+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
     du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
 
     salaire de base = coefficient * valeur du point de l'Implantation du Poste
     """
 
+    objects = models.Manager()
     actuelles = ValeurPointManager()
 
     valeur = models.FloatField(null=True)
@@ -1335,36 +1820,42 @@ 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' % (self.devise.code, self.annee, self.valeur, self.implantation.nom_court, self.devise.nom)
+        return u'%s %s %s [%s] %s' % (
+            self.devise.code, self.annee, self.valeur,
+            self.implantation.nom_court, self.devise.nom
+        )
 
 
 class ValeurPoint(ValeurPoint_):
     __doc__ = ValeurPoint_.__doc__
 
+reversion.register(ValeurPoint, format='xml')
 
 
-class Devise(AUFMetadata):
-    """Devise monétaire.
+class Devise(Archivable):
     """
-    
-    objects = DeviseManager()
-
-    archive = models.BooleanField(verbose_name=u"Archivé", default=False)
-    code =  models.CharField(max_length=10, unique=True)
+    Devise monétaire.
+    """
+    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)
 
-class TypeContrat(AUFMetadata):
-    """Type de contrat.
+reversion.register(Devise, format='xml')
+
+
+class TypeContrat(Archivable):
+    """
+    Type de contrat.
     """
     nom = models.CharField(max_length=255)
     nom_long = models.CharField(max_length=255)
@@ -1377,19 +1868,36 @@ class TypeContrat(AUFMetadata):
     def __unicode__(self):
         return u'%s' % (self.nom)
 
+reversion.register(TypeContrat, format='xml')
+
 
 ### AUTRES
 
-class ResponsableImplantation(AUFMetadata):
-    """Le responsable d'une implantation.
+class ResponsableImplantationProxy(ref.Implantation):
+
+    def save(self):
+        pass
+
+    class Meta:
+        managed = False
+        proxy = True
+        verbose_name = u"Responsable d'implantation"
+        verbose_name_plural = u"Responsables d'implantation"
+
+
+class ResponsableImplantation(models.Model):
+    """
+    Le responsable d'une implantation.
     Anciennement géré sur le Dossier du responsable.
     """
-    employe = models.ForeignKey('Employe', db_column='employe',
-                            related_name='+',
-                            null=True, blank=True)
-    implantation = models.ForeignKey(ref.Implantation,
-                            db_column='implantation', related_name='+',
-                            unique=True)
+    employe = models.ForeignKey(
+        'Employe', db_column='employe', related_name='+', null=True,
+        blank=True
+    )
+    implantation = models.OneToOneField(
+        "ResponsableImplantationProxy", db_column='implantation',
+        related_name='responsable', unique=True
+    )
 
     def __unicode__(self):
         return u'%s : %s' % (self.implantation, self.employe)
@@ -1399,3 +1907,222 @@ class ResponsableImplantation(AUFMetadata):
         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)
+
+