merge regionalisation
authorOlivier Larchevêque <olivier.larcheveque@auf.org>
Fri, 6 Jul 2012 20:40:03 +0000 (16:40 -0400)
committerOlivier Larchevêque <olivier.larcheveque@auf.org>
Fri, 6 Jul 2012 20:40:03 +0000 (16:40 -0400)
12 files changed:
1  2 
project/dae/views.py
project/decorators.py
project/menu.py
project/rh/admin.py
project/rh/graph.py
project/rh/managers.py
project/rh/models.py
project/rh/test/common.py
project/rh/views.py
project/settings.py
project/templates/menu.html
src/auf.django.metadata/auf/django/metadata/managers.py

@@@ -319,13 -288,12 +321,13 @@@ def embauche_consulter(request, dossier
          'dossier': dossier,
          'validationForm': validationForm,
          'comparaisons_internes': comparaisons_internes,
-         'comparaisons': comparaisons
-     }
-     return render(request, 'dae/embauche_consulter.html', vars)
+         'comparaisons': comparaisons,
+         'importer': in_drh_or_admin(request.user)
+     })
  
 -
 -@drh_or_admin_required
 +@user_passes_test(lambda u: u.is_superuser)
 +@dae_groupe_requis
 +@dossier_dans_ma_region_ou_service
  def embauche_importer(request, dossier_id=None):
      dossier_dae = get_object_or_404(dae.Dossier, id=dossier_id)
      if request.method == 'POST':
@@@ -6,10 -6,11 +6,10 @@@ from django.http import HttpResponseRed
  from django.conf import settings
  from django.contrib import messages
  from django.contrib.auth import REDIRECT_FIELD_NAME
 -from django.contrib.auth.decorators import user_passes_test
 -from django.core.urlresolvers import reverse
 +from django.db.models import Q
  from django.utils.http import urlquote
  
--from project.groups import grp_drh, grp_drh2, grp_correspondants_rh
++from project import groups
  from project.groups import get_employe_from_user
  
  
@@@ -31,10 -28,10 +31,10 @@@ def in_drh_or_admin(user)
      """
      Teste si un user Django fait parti du groupe DRH, DRH2 ou s'il est admin
      """
--    groups = user.groups.all()
++    user_groups = user.groups.all()
      if user.is_superuser or \
--            grp_drh in groups or \
--            grp_drh2 in groups:
++            groups.grp_drh in user_groups or \
++            groups.grp_drh2 in user_groups:
          return True
      else:
          return False
@@@ -62,9 -57,9 +62,12 @@@ def region_protected(model)
              if request.user.is_superuser:
                  return func(request, id)
              user_groups = request.user.groups.all()
--            if grp_drh in user_groups:
++            if groups.grp_drh in user_groups or \
++               groups.grp_drh2 in user_groups:
                  return func(request, id)
--            if grp_correspondants_rh in user_groups:
++            if groups.grp_correspondants_rh in user_groups or \
++               groups.grp_administrateurs in user_groups or \
++               groups.grp_directeurs_bureau in user_groups:
                  employe = get_employe_from_user(request.user)
                  q = Q(**{
                      model.prefix_implantation: employe.implantation.region
diff --cc project/menu.py
@@@ -14,8 -14,9 +14,10 @@@ from django.utils.translation import ug
  from admin_tools.menu import items, Menu
  
  from project.decorators import in_drh_or_admin
+ from project import groups
  
 +
  class CustomMenu(Menu):
      """
      Custom Menu for project admin site.
          Use this method if you need to access the request context.
          """
          request = context['request']
 +
 +        if request.user.is_superuser:
 +            self.children += [
 +                    items.MenuItem('DAE', reverse('admin:app_list',
 +                        kwargs={'app_label': 'dae'})),
 +                    ]
 +
-         if in_drh_or_admin(request.user):
+         user_groups = request.user.groups.all()
+         if in_drh_or_admin(request.user) or\
+            groups.grp_correspondants_rh in user_groups or\
+            groups.grp_administrateurs in user_groups or\
+            groups.grp_directeurs_bureau in user_groups:
              self.children += [
                  items.MenuItem('Rapports',
 -                               children=[
 -                                   #items.MenuItem('Rapport des postes', reverse('rhr_postes')),
 -                                   items.MenuItem('Rapport des contrats', reverse('rhr_contrats')),
 -                                   items.MenuItem(u'Rapport des employés sans contrat', reverse('rhr_employe_sans_contrat')),
 -                                   #items.MenuItem('Rapport de rémunération', reverse('rhr_remuneration')),
 +                    children=[
 +                        #items.MenuItem('Rapport des postes',
 +                        #     reverse('rhr_postes')),
 +                        items.MenuItem('Rapport des contrats',
 +                            reverse('rhr_contrats')),
 +                        items.MenuItem(u'Rapport des employés sans contrat',
 +                            reverse('rhr_employe_sans_contrat')),
 +                        #items.MenuItem('Rapport de rémunération',
 +                        #     reverse('rhr_remuneration')),
 +
 +                        # A corriger
 +                        #items.MenuItem('Rapport des postes par service',
 +                        #    reverse('rhr_postes_service')),
 +                        #items.MenuItem('Rapport des postes par implantation',
 +                        #     reverse('rhr_postes_implantation')),
 +
 +                        #items.MenuItem('Modelisation des postes',
 +                        #     reverse('rhr_postes_modelisation')),
 +                        #items.MenuItem('Rapport hiérarchique des postes',
 +                        #     reverse('rhr_postes_hierarchie')),
 +                        items.MenuItem('Rapport de masse salariale',
 +                            reverse('rhr_masse_salariale')),
 +                        items.MenuItem('Rapport des modifications',
 +                            reverse('rhr_historique_des_modifications')),
 +                        ]),
  
 -                                   # A corriger
 -                                   #items.MenuItem('Rapport des postes par service', reverse('rhr_postes_service')),
 -                                   #items.MenuItem('Rapport des postes par implantation', reverse('rhr_postes_implantation')),
 -                                   
 -                                   #items.MenuItem('Modelisation des postes', reverse('rhr_postes_modelisation')),
 -                                   #items.MenuItem('Rapport hiérarchique des postes', reverse('rhr_postes_hierarchie')),
 -                                   items.MenuItem('Rapport de masse salariale', reverse('rhr_masse_salariale')),
 -                               ]
 -                              ),
                  items.MenuItem('Organigrammes',
                      children=[
 -                        items.MenuItem('Organigramme par employé', reverse('admin:rh_employeproxy_changelist')),
 -                        items.MenuItem('Organigramme par service', reverse('admin:rh_serviceproxy_changelist')),
 -                        items.MenuItem('Organigramme par implantation', reverse('admin:rh_implantationproxy_changelist')),
 -                        items.MenuItem('Organigramme par bureau', reverse('admin:rh_regionproxy_changelist')),
 -                        ]
 -                    ),
 +                        items.MenuItem('Organigramme par employé',
 +                            reverse('admin:rh_employeproxy_changelist')),
 +                        items.MenuItem('Organigramme par service',
 +                            reverse('admin:rh_serviceproxy_changelist')),
 +                        items.MenuItem('Organigramme par implantation',
 +                            reverse('admin:rh_implantationproxy_changelist')),
 +                        items.MenuItem('Organigramme par bureau',
 +                            reverse('admin:rh_regionproxy_changelist')),
 +                        ]),
                  items.MenuItem('Requêtes',
                      children=[
 -                        items.MenuItem('Requêtes sauvegardées', reverse('admin:django_qbe_savedquery_changelist')),
 -                        items.MenuItem('Constructeur de requêtes', reverse('qbe_form')),
 -                        ]
 -                    ),
 +                        items.MenuItem('Requêtes sauvegardées',
 +                            reverse('admin:django_qbe_savedquery_changelist')),
 +                        items.MenuItem('Constructeur de requêtes',
 +                            reverse('qbe_form')),
 +                        ]),
              ]
          super(CustomMenu, self).init_with_context(context)
 -        
@@@ -12,15 -7,26 +12,25 @@@ from django.contrib.contenttypes.model
  from django.conf import settings
  from django.db.models import Q, Count
  from django.template.defaultfilters import date
 -
 -from ajax_select import make_ajax_form
 -
 -from auf.django.metadata.admin import \
 -        AUFMetadataAdminMixin, AUFMetadataInlineAdminMixin, \
 -        AUF_METADATA_READONLY_FIELDS
 -import auf.django.references.models as ref
 +from django.utils.formats import date_format
  
+ from project import groups
  from project.decorators import in_drh_or_admin
 +from project.groups import grp_correspondants_rh
 +from project.groups import get_employe_from_user
 +from project.rh import models as rh
+ from project.permissions import get_region_user,  \
+         user_gere_obj_de_sa_region, \
+         user_can_add_obj, \
+         user_can_change_obj, \
+         user_can_delete_obj
+ import project.rh.models as rh
+ from project.rh.forms import \
+         ContratForm, AyantDroitForm, EmployeAdminForm, AjaxSelect, DossierForm
  from project.rh.change_list import ChangeList
 +from project.rh.forms import ContratForm, AyantDroitForm, EmployeAdminForm, \
 +          AjaxSelect, DossierForm, ResponsableInlineForm
  
  
  class BaseAdmin(admin.ModelAdmin):
              'jquery-autocomplete/jquery.autocomplete.min.js',
          )
  
 +# Admin pour reversion
  
 -class ArchiveMixin(object):
 +class ArchivableAdmin(admin.ModelAdmin):
      """
 -    Archive Mixin pour gérer le queryset et le display
 -    NON COMPRIS : list_filter, et list_display, field à setter dans la classe.
 +    Admin pour les modèles archivables
      """
 +    list_filter = ('archive', )
  
      def queryset(self, request):
 -        return self.model._base_manager
 +        return self.model.avec_archives.all()
  
      def _archive(self, obj):
          if obj.archive:
@@@ -136,82 -143,17 +148,70 @@@ class ProtectRegionMixin(object)
          return qs.none()
  
      def has_add_permission(self, request):
-         if not in_drh_or_admin(request.user):
-             return False
-         else:
-             return True
+         return user_can_add_obj(request.user)
  
      def has_change_permission(self, request, obj=None):
-         user_groups = request.user.groups.all()
+         return user_can_change_obj(request.user, obj) if obj else True
  
-         # Lock pour autoriser uniquement les DRH à utiliser RH
-         if not in_drh_or_admin(request.user):
-             return False
-         if len(user_groups) == 0 and not request.user.is_superuser:
-             return False
-         if obj is None:
-             return True
-         ids = [o.id for o in self.queryset(request)]
-         return obj.id in ids
+     def has_delete_permission(self, request, obj=None):
+         return user_can_delete_obj(request.user, obj) if obj else True
  
  
 +class DerniereModificationAdmin(admin.ModelAdmin):
 +
 +    def queryset(self, request):
 +        qs = super(DerniereModificationAdmin, self).queryset(request)
 +        ct = ContentType.objects.get_for_model(self.model)
 +        db_table = self.model._meta.db_table
 +        pk = self.model._meta.pk.column
 +        return qs.extra(select={
 +            'date_modification':
 +            "SELECT action_time FROM django_admin_log "
 +            "WHERE content_type_id = %d AND object_id = %s.%s "
 +            "ORDER BY action_time DESC "
 +            "LIMIT 1" % (ct.id, db_table, pk),
 +            'user_modification':
 +            "SELECT u.username "
 +            "FROM auth_user u "
 +            "INNER JOIN django_admin_log l ON l.user_id = u.id "
 +            "WHERE l.content_type_id = %d AND object_id = %s.%s "
 +            "ORDER BY action_time DESC "
 +            "LIMIT 1" % (ct.id, db_table, pk),
 +        })
 +
 +    def derniere_modification(self, obj):
 +        text = ''
 +        if obj.date_modification:
 +            text += obj.date_modification.strftime('%d-%m-%Y %H:%M')
 +        if obj.user_modification:
 +            text += ' par ' + obj.user_modification
 +        return text
 +    derniere_modification.short_description = u'dernière modification'
 +    derniere_modification.admin_order_field = 'date_modification'
 +
 +
  # Inlines
  
 +class CommentaireInlineForm(forms.ModelForm):
 +
 +    def save(self, commit=True):
 +
 +        # Hack: reversion.VersionAdmin ne sauvegarde pas les champs qui ne
 +        # sont pas explicitement dans le formulaire. Il plante cependant
 +        # leur valeur dans `self.initial`. Ceci est un peu fragile. Si
 +        # c'est possible, il serait plus approprié que Reversion se rende
 +        # compte qu'il manque des champs.
 +        instance = super(CommentaireInlineForm, self).save(commit=False)
 +        if instance.owner_id is None and 'owner' in self.initial:
 +            instance.owner_id = self.initial['owner']
 +        if instance.date_creation is None and 'date_creation' in self.initial:
 +            instance.date_creation = self.initial['date_creation']
 +        if commit:
 +            instance.save()
 +            self.save_m2m()
 +        return instance
 +
 +
  class ReadOnlyInlineMixin(object):
  
      def get_readonly_fields(self, request, obj=None):
@@@ -1012,20 -969,11 +1030,21 @@@ class ResponsableInline(admin.TabularIn
  
  class ResponsableImplantationAdmin(BaseAdmin):
      actions = None
 +    fields = ('nom', )
 +    inlines = (ResponsableInline, )
      list_filter = ('region', 'statut', )
      list_display = ('_region', '_nom', 'statut', '_responsable', )
 +    list_display_links = ('_nom',)
 +    list_per_page = 500
      readonly_fields = ('nom', )
 -    fields = ('nom', )
 +    search_fields = (
 +            'nom',
 +            'responsable__employe__id',
 +            'responsable__employe__nom',
 +            'responsable__employe__prenom',
 +            )
 +    ordering = ('nom',)
+     inlines = (ResponsableInline, )
  
      def _region(self, obj):
          return obj.region.code
@@@ -14,9 -19,10 +19,10 @@@ def bind_poste_to_graph(user, graph, po
          dossiers = rh.Dossier.objects.select_related('employe').filter(
                  (Q(date_fin__gt=date.today()) | Q(date_fin=None)) &
                  (Q(date_debut__lt=date.today()) | Q(date_debut=None)) &
 -                Q(poste=poste)
 -        ).exclude(supprime=True).all()
 +                Q(poste__id=n)
 +        ).all()
  
+         # affichage
          if dossiers:
              employes = "\\n".join(
                          ["[%s] %s %s" %
@@@ -1,16 -1,22 +1,15 @@@
  # -*- encoding: utf-8 -*-
  
--import datetime
 +from datetime import date
  
- from auf.django.metadata.managers import NoDeleteManager, NoDeleteQuerySet
  from django.db import models
  from django.db.models import Q
 -
 -from auf.django.metadata.managers import NoDeleteManager
++from django.db.models.query import QuerySet
  
  from project.groups import get_employe_from_user
 -from project.groups import grp_administrateurs, \
 -                     grp_directeurs_bureau, \
 -                     grp_drh, \
 -                     grp_drh2, \
 -                     grp_accior, \
 -                     grp_abf, \
 -                     grp_haute_direction, \
 -                     grp_service_utilisateurs, \
 -                     grp_correspondants_rh
 +from project.groups import \
 +        grp_drh, grp_drh2, grp_accior, grp_abf, grp_haute_direction, \
 +        grp_service_utilisateurs
  
  
  class SecurityManager(models.Manager):
          return liste
  
  
 -class PosteManager(SecurityManager, NoDeleteManager):
 -    """
 -    Chargement de tous les objets FK existants sur chaque QuerySet.
 -    """
++class ActifsQuerySet(QuerySet):
++
++    def _actifs(self, debut_field, fin_field, date_min=None, date_max=None,
++                annee=None):
++        qs = self
++        if annee:
++            janvier = date(annee, 1, 1)
++            decembre = date(annee, 12, 31)
++            date_min = max(janvier, date_min) if date_min else janvier
++            date_max = min(decembre, date_max) if date_max else decembre
++        if not date_min and not date_max:
++            date_min = date_max = date.today()
++        if date_min:
++            qs = qs.filter(
++                Q(**{fin_field + '__gte': date_min}) |
++                Q(**{fin_field: None})
++            )
++        if date_max:
++            qs = qs.filter(
++                Q(**{debut_field + '__lte': date_max}) |
++                Q(**{debut_field: None})
++            )
++        return qs
++
++    def actifs(self, *args, **kwargs):
++        return self._actifs('date_debut', 'date_fin', *args, **kwargs)
++
++
++class PosteQuerySet(ActifsQuerySet):
++    pass
++
++
 +class PosteManager(SecurityManager):
      prefixe_service = "service"
      prefixe_implantation = "implantation__region"
  
--    def actifs(self):
-         return super(PosteManager, self).get_query_set().filter(
-             Q(date_fin__gt=datetime.datetime.now()) |
-             Q(date_fin__isnull=True)
-         )
 -        q_actif = Q(date_fin__gt=datetime.datetime.now()) | Q(date_fin__isnull=True)
 -        return super(PosteManager, self).get_query_set().filter(q_actif)
++    def get_query_set(self):
++        return PosteQuerySet(self.model).select_related('type_poste')
++
++    def actifs(self, *args, **kwargs):
++        return self.get_query_set().actifs(*args, **kwargs)
  
      def ma_region_ou_service(self, user):
          return super(PosteManager, self).ma_region_ou_service(user)
  
--    def get_query_set(self):
--        fkeys = (
--            #'id_rh',
--            #'responsable',
--            #'implantation',
--            #'implantation.bureau_rattachement',
--            'type_poste',
--            #'service',
--            #'classement_min',
--            #'classement_max',
--            #'valeur_point_min',
--            #'valeur_point_max',
--        )
--        return super(PosteManager, self).get_query_set() \
--                                        .select_related(*fkeys).all()
 -
 -
 -class DossierManager(SecurityManager, NoDeleteManager):
++
++class DossierQuerySet(ActifsQuerySet):
++    pass
 +
 +
 +class DossierManager(SecurityManager):
      prefixe_service = "poste__service"
      prefixe_implantation = "poste__implantation__region"
  
      def get_query_set(self):
--        fkeys = (
--            'poste',
--            'employe',
--        )
--        return super(DossierManager, self).get_query_set() \
--                                        .select_related(*fkeys).all()
++        return DossierQuerySet(self.model) \
++                .select_related('poste', 'employe')
  
--    def ma_region_ou_service(self, user):
--        return super(DossierManager, self).ma_region_ou_service(user)
++    def actifs(self, *args, **kwargs):
++        return self.get_query_set().actifs(*args, **kwargs)
 +
 +
- class EmployeQuerySet(NoDeleteQuerySet):
++class RemunerationQuerySet(ActifsQuerySet):
++
++    def actifs(self, *args, **kwargs):
++        return self \
++                ._actifs('date_debut', 'date_fin', *args, **kwargs) \
++                ._actifs(
++                    'dossier__date_debut', 'dossier__date_fin', *args, **kwargs
++                )
++
++
++class RemunerationManager(models.Manager):
++
++    def get_query_set(self):
++        return RemunerationQuerySet(self.model)
++
++    def actifs(self, *args, **kwargs):
++        return self.get_query_set().actifs(*args, **kwargs)
++
++
++class EmployeQuerySet(ActifsQuerySet):
 +
 +    def actifs(self, date_min=None, date_max=None, annee=None):
-         qs = self
-         if annee:
-             janvier = date(annee, 1, 1)
-             decembre = date(annee, 12, 31)
-             date_min = max(janvier, date_min) if date_min else janvier
-             date_max = min(decembre, date_max) if date_max else decembre
-         if not date_min and not date_max:
-             date_min = date_max = date.today()
-         if date_min:
-             qs = qs.filter(
-                 Q(rh_dossiers__date_fin__gte=date_min) |
-                 Q(rh_dossiers__date_fin=None)
-             )
-         if date_max:
-             qs = qs.filter(
-                 Q(rh_dossiers__date_debut__lte=date_max) |
-                 Q(rh_dossiers__date_debut=None)
-             )
-         return qs.distinct()
++        return self \
++                ._actifs('rh_dossiers__date_debut', 'rh_dossiers__date_fin') \
++                .distinct()
++
 +
++class EmployeManager(models.Manager):
 +
- class EmployeManager(NoDeleteManager):
 +    def get_query_set(self):
 +        return EmployeQuerySet(self.model)
 +
 +    def actifs(self, *args, **kwargs):
 +        return self.get_query_set().actifs(*args, **kwargs)
  
  
  class PosteComparaisonManager(SecurityManager):
@@@ -160,5 -131,11 +176,13 @@@ class DossierComparaisonManager(Securit
      prefixe_implantation = "implantation__region"
  
  
 -class DeviseManager(NoDeleteManager):
++class DeviseManager(models.Manager):
+     pass
 -class ServiceManager(NoDeleteManager):
++
++class ServiceManager(models.Manager):
+     pass
 -class TypeRemunerationManager(NoDeleteManager):
++
 +class TypeRemunerationManager(models.Manager):
      pass
@@@ -16,14 -12,18 +16,15 @@@ from django.conf import setting
  from project.rh.change_list import \
          RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \
          STATUT_FUTUR
 -from project.rh.managers import \
 -        PosteManager, DossierManager, DossierComparaisonManager, \
 -        PosteComparaisonManager, DeviseManager, ServiceManager, \
 -        TypeRemunerationManager
 +from project.rh.managers import PosteManager, DossierManager, EmployeManager, \
 +        DossierComparaisonManager, \
 +        PosteComparaisonManager, \
-         TypeRemunerationManager
++        TypeRemunerationManager, \
++        RemunerationManager
  from project.rh.validators import validate_date_passee
  
 +# import pour relocaliser le modèle selon la convention
 +from project.rh.historique import ModificationTraite
  
  # Constantes
  HELP_TEXT_DATE = "format: jj-mm-aaaa"
@@@ -1165,6 -1074,6 +1166,7 @@@ class Remuneration_(RemunerationMixin, 
      pour un Dossier. Si un Evenement existe, utiliser la structure de
      rémunération EvenementRemuneration de cet événement.
      """
++    objects = RemunerationManager()
  
      def montant_mois(self):
          return round(self.montant / 12, 2)
@@@ -1625,10 -1510,8 +1627,11 @@@ reversion.register(TypeContrat, format=
  
  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"
index 0000000,a9aa4d5..3680e16
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,252 +1,252 @@@
+ # -*- coding: utf-8 -*-
+ import datetime
+ from django.contrib.auth.models import User
 -from project.rh import groups
++from project import groups
+ from auf.django.references import models as ref
+ from django.test import TestCase
+ from project.rh import models as rh
+ class RhTest(TestCase):
+     def setUp(self):
+         """
+         POSTES
+         ======
+         self.poste_cnf_ngaoundere
+         self.poste_cnf_bangui
+         self.poste_bap_bureau
+         self.poste_bap_ifi
+         EMPLOYES
+         ========
+         0@test.auf: self.employe_cnf_ngaoundere
+         1@test.auf: self.employe_cnf_bangui
+         2@test.auf: self.employe_bap_bureau
+         3@test.auf: self.employe_bap_ifi
+         USERS DJANGO
+         ============
+         cf. EMPLOYES
+         DOSSIERS
+         ========
+         self.dossier_cnf_ngaoundere
+         self.dossier_cnf_bangui
+         self.dossier_bap_bureau
+         self.dossier_bap_ifi
+         """
+         self.password = "0000"
+         self.today = datetime.datetime.now()
+         #########################
+         # Régions / Implantations
+         #########################
+         self.REGION_ACGL = ref.Region.objects.get(id=1)
+         self.IMPLANTATION_ACGL_CNF_NGAOUNDERE = ref.Implantation.objects.get(id=90)
+         self.IMPLANTATION_ACGL_CNF_BANGUI = ref.Implantation.objects.get(id=85)
+         self.REGION_BAP = ref.Region.objects.get(id=4)
+         self.IMPLANTATION_BAP_BUREAU = ref.Implantation.objects.get(id=51)
+         self.IMPLANTATION_BAP_IFI = ref.Implantation.objects.get(id=55)
+         ##########
+         # Employés
+         ##########
+         self.employe_cnf_ngaoundere = rh.Employe(
+                 id=1,
+                 nom="Employé",
+                 prenom="ngaoundere",
+                 )
+         self.employe_cnf_ngaoundere.save()
+         self.employe_cnf_bangui = rh.Employe(
+                 id=2,
+                 nom="Employé",
+                 prenom="bangui",
+                 )
+         self.employe_cnf_bangui.save()
+         self.employe_bap_bureau = rh.Employe(
+                 id=3,
+                 nom="Employé",
+                 prenom="BAP bureau",
+                 )
+         self.employe_bap_bureau.save()
+         self.employe_bap_ifi = rh.Employe(
+                 id=4,
+                 nom="Employé",
+                 prenom="BAP IFI",
+                 )
+         self.employe_bap_ifi.save()
+         ######
+         # ACGL
+         ######
+         self.poste_cnf_ngaoundere = rh.Poste(nom="poste à NGAOUNDERE",
+                 implantation=self.IMPLANTATION_ACGL_CNF_NGAOUNDERE)
+         self.poste_cnf_bangui = rh.Poste(nom="poste à BANGUI",
+                 implantation=self.IMPLANTATION_ACGL_CNF_BANGUI)
+         self.poste_cnf_ngaoundere.save()
+         self.poste_cnf_bangui.save()
+         self.dossier_cnf_ngaoundere = rh.Dossier(poste=self.poste_cnf_ngaoundere,
+             employe=self.employe_cnf_ngaoundere, date_debut=self.today)
+         self.dossier_cnf_bangui = rh.Dossier(poste=self.poste_cnf_bangui,
+             employe=self.employe_cnf_bangui, date_debut=self.today)
+         self.dossier_cnf_ngaoundere.save()
+         self.dossier_cnf_bangui.save()
+         #####
+         # BAP
+         #####
+         self.poste_bap_bureau = rh.Poste(nom="poste au Bureau BAP",
+                 implantation=self.IMPLANTATION_BAP_BUREAU)
+         self.poste_bap_ifi = rh.Poste(nom="poste à l'IFI",
+                 implantation=self.IMPLANTATION_BAP_IFI)
+         self.poste_bap_bureau.save()
+         self.poste_bap_ifi.save()
+         self.dossier_bap_bureau = rh.Dossier(poste=self.poste_bap_bureau,
+             employe=self.employe_bap_bureau, date_debut=self.today)
+         self.dossier_bap_ifi = rh.Dossier(poste=self.poste_bap_ifi,
+             employe=self.employe_bap_ifi, date_debut=self.today)
+         self.dossier_bap_bureau.save()
+         self.dossier_bap_ifi.save()
+         ##############
+         # Users Django
+         ##############
+         self._clean_refs()
+         for idx, e in enumerate(rh.Employe.objects.all().order_by('id')):
+             email = u"%s@test.auf" % idx
+             u = User(first_name=e.prenom,
+                     is_staff=True,
+                     last_name=e.nom,
+                     username=email,
+                     email=email)
+             u.set_password(self.password)
+             u.save()
+             
+             # on le porte dans le référentiel employé
+             ref_e = ref.Employe(id=u.id,
+                     nom=e.nom, prenom=e.prenom,
+                     implantation=e.poste_principal().implantation,
+                     implantation_physique=e.poste_principal().implantation,
+                     service=ref.Service.objects.get(id=1))
+             ref_e.save()
+             # on le porte dans le référentiel auth
+             ref.Authentification(id=ref_e, courriel=u.email).save()
+     def _clean_refs(self):
+         courriels = [u"%s@test.auf" % idx for idx, e in
+                 enumerate(rh.Employe.objects.all())]
+         ref.Employe.objects.filter(courriel__in=courriels).delete()
+         ref.Authentification.objects.filter(courriel__in=courriels).delete()
+     def tearDown(self):
+         self._clean_refs()
+     def _test_acces_ok(self, url):
+         response = self.client.get(url, follow=True)
+         self.assertEqual(response.status_code, 200)
+         self.assertEqual('next' in response.context, False)
+     def _test_acces_ko(self, url):
+         response = self.client.get(url, follow=True)
+         is_ko = response.status_code == 403 or 'next' in response.context
+         self.assertEqual(is_ko, True)
+     def _test_anonyme(self):
+         pass
+     def _test_correspondant_rh(self, email="0@test.auf"):
+         u = User.objects.get(email=email)
+         groups.grp_correspondants_rh.user_set.add(u)
+         groups.grp_correspondants_rh.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     def _test_administrateur_regional(self, email="0@test.auf"):
+         u = User.objects.get(email=email)
+         groups.grp_administrateurs.user_set.add(u)
+         groups.grp_administrateurs.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     def _test_directeur_bureau(self, email="0@test.auf"):
+         u = User.objects.get(email=email)
+         groups.grp_directeurs_bureau.user_set.add(u)
+         groups.grp_directeurs_bureau.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     def _test_drh(self):
+         email = "0@test.auf"
+         
+         u = User.objects.get(email=email)
+         groups.grp_drh.user_set.add(u)
+         groups.grp_drh.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     def _test_drh2(self):
+         email = "0@test.auf"
+         
+         u = User.objects.get(email=email)
+         groups.grp_drh2.user_set.add(u)
+         groups.grp_drh2.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     def _test_grp_accior(self):
+         email = "0@test.auf"
+         
+         u = User.objects.get(email=email)
+         groups.grp_accior.user_set.add(u)
+         groups.grp_accior.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     
+     def _test_grp_abf(self):
+         email = "0@test.auf"
+         
+         u = User.objects.get(email=email)
+         groups.grp_abf.user_set.add(u)
+         groups.grp_abf.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     def _test_grp_haute_direction(self):
+         email = "0@test.auf"
+         
+         u = User.objects.get(email=email)
+         groups.grp_haute_direction.user_set.add(u)
+         groups.grp_haute_direction.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
+     def _test_grp_service_utilisateurs(self):
+         email = "0@test.auf"
+         
+         u = User.objects.get(email=email)
+         groups.grp_service_utilisateurs.user_set.add(u)
+         groups.grp_service_utilisateurs.save()
+         credentials = {'username': email, 'password': self.password}
+         self.assertTrue(self.client.login(**credentials), "login failed")
@@@ -13,28 -13,31 +13,31 @@@ from django.contrib.auth.decorators imp
  from django.core.urlresolvers import reverse
  from django.db.models import Q
  from django.http import HttpResponse
 -from django.shortcuts import render, get_object_or_404
 -
 -from auf.django.references import models as ref
 +from django.shortcuts import render, get_object_or_404, redirect
  
 -from project import groups
 -from project.decorators import drh_or_admin_required, in_one_of_group, \
 -        in_drh_or_admin
 -from project.decorators import redirect_interdiction
 +from project.decorators import drh_or_admin_required
  from project.decorators import region_protected
- from project.groups import \
-         get_employe_from_user, grp_drh, grp_correspondants_rh
+ from project.groups import get_employe_from_user
++from project import groups
++from project.decorators import in_one_of_group, in_drh_or_admin
 -from project.rh import models as rh
 +from project.rh import ods
  from project.rh import graph as rh_graph
 +from project.rh import models as rh
  from project.rh.change_list import RechercheTemporelle
 -from project.rh.lib import calc_remun, get_lookup_params
 -from project.rh.masse_salariale import MasseSalariale
 +from project.rh.forms import MasseSalarialeForm
 +from project.rh.lib import get_lookup_params
  from project.rh.templatetags.rapports import SortHeaders
 +from project.rh.historique import get_active_revisions, TodoForm
 +
 +TWOPLACES = Decimal('0.01')
  
  
  @login_required
+ @drh_or_admin_required
  def profil(request):
      """Profil personnel de l'employé - éditable"""
 -    employe = get_employe_from_user(user)
 +    employe = get_employe_from_user(request.user)
      c = {
        'user': request.user,
        'employe': employe,
@@@ -72,9 -77,12 +77,13 @@@ def employe(request, id)
  
  
  @login_required
- @drh_or_admin_required
+ @in_one_of_group((groups.grp_correspondants_rh,
+     groups.grp_administrateurs,
+     groups.grp_directeurs_bureau,
+     groups.grp_drh,
+     groups.grp_drh2))
  def rapports_contrat(request):
 +    # statut default = actif?
      if 'HTTP_REFERER' in request.META.keys():
          referer = request.META['HTTP_REFERER']
          referer = "/".join(referer.split('/')[3:])
      lookup_params = cl.purge_params(lookup_params)
      q_temporel = cl.get_q_temporel(contrats)
      q = Q(**lookup_params) & q_temporel
 +    contrats = contrats.filter(q)
+     user_groups = request.user.groups.all()
+     if groups.grp_correspondants_rh in user_groups or\
+        groups.grp_administrateurs in user_groups or\
+        groups.grp_directeurs_bureau in user_groups:
+         employe = get_employe_from_user(request.user)
+         q = q & Q(dossier__poste__implantation__region=employe.implantation.region)
 -    contrats = contrats.filter(q).exclude(dossier__employe__supprime=1)
++    contrats = contrats.filter(q)
  
 +    # tri
      if 'o' in request.GET:
          contrats = contrats.order_by(
              ('-' if request.GET.get('ot') == "desc" else '') + request.GET['o']
  
  
  @login_required
- @drh_or_admin_required
+ @in_one_of_group((groups.grp_correspondants_rh,
+     groups.grp_administrateurs,
+     groups.grp_directeurs_bureau,
+     groups.grp_drh,
+     groups.grp_drh2))
 -def rapports_masse_salariale(request):
 -
 -    class RechercheTemporelle(forms.Form):
 -        remunerations = rh.Remuneration.objects.exclude(date_debut=None)
 -        if len(remunerations) > 0:
 -            annee_debut = remunerations.order_by('date_debut')[0].date_debut.year
 -        else:
 -            annee_debut = 1982
 -        CHOICE_ANNEES = range(annee_debut, date.today().year + 1)
 -
 -        annee = forms.CharField(
 -            initial=date.today().year,
 -            widget=forms.Select(
 -                choices=((a, a) for a in reversed(CHOICE_ANNEES))
 -            )
 -        )
 -
 -        user_groups = request.user.groups.all()
 -        if groups.grp_correspondants_rh in user_groups or\
 -           groups.grp_administrateurs in user_groups or\
 -           groups.grp_directeurs_bureau in user_groups:
 -            employe = get_employe_from_user(request.user)
 -            regions = ref.Region.objects.filter(id=employe.implantation.region.id)
 -            implantations = ref.Implantation.objects.filter(region=employe.implantation.region)
 -        else:
 -            regions = ref.Region.objects.all()
 -            implantations = ref.Implantation.objects.all()
 -
 -        region = forms.CharField(
 -                widget=forms.Select(choices=[('', '')] +
 -                    [(i.id, i) for i in regions]
 -                )
 -            )
 -
 -        implantation = forms.CharField(
 -                widget=forms.Select(choices=[('', '')] +
 -                    [(i.id, i) for i in implantations]
 -                )
 -            )
 -
 -        #date_debut = forms.DateField(widget=adminwidgets.AdminDateWidget)
 -        #date_fin = forms.DateField(widget=adminwidgets.AdminDateWidget)
 -
 -    form = RechercheTemporelle(request.GET)
 -    get_filtre = [
 -        (k, v) for k, v in request.GET.items()
 -        if k not in ('date_debut', 'date_fin', 'implantation')
 -    ]
 -    query_string = urllib.urlencode(get_filtre)
 -
 -    date_debut = None
 -    date_fin = None
 -    if request.GET.get('annee', None):
 -        date_debut = "01-01-%s" % request.GET.get('annee', None)
 -        date_fin = "31-12-%s" % request.GET.get('annee', None)
 -
 -    implantation = request.GET.get('implantation')
 -    region = request.GET.get('region')
 -
 -    custom_filter = {}
 -    if implantation:
 -        custom_filter['dossier__poste__implantation'] = implantation
 -    if region:
 -        custom_filter['dossier__poste__implantation__region'] = region
 -
 -    # on force la région dans tous les cas pour les membres de ce groupe
 -    user_groups = request.user.groups.all()
 -    if groups.grp_correspondants_rh in user_groups or\
 -       groups.grp_administrateurs in user_groups or\
 -       groups.grp_directeurs_bureau in user_groups:
 -        employe = get_employe_from_user(request.user)
 -        custom_filter['dossier__poste__implantation__region'] = employe.implantation.region.id
 -
 -
 -    c = {
 -            'title': 'Rapport de masse salariale',
 -            'form': form,
 -            'headers': [],
 -            'query_string': query_string,
 -    }
 -    if date_debut or date_fin:
 -        masse = MasseSalariale(date_debut, date_fin, custom_filter,
 -                request.GET.get('ne_pas_grouper', False))
 -        if masse.rapport:
 -            if request.GET.get('ods'):
 -                for h in (
 -                    h for h in masse.headers if 'background-color' in h[2]
 -                ):
 -                    del h[2]['background-color']
 -                masse.ods()
 -                output = StringIO.StringIO()
 -                masse.doc.save(output)
 -                output.seek(0)
 -
 -                response = HttpResponse(
 -                    FileWrapper(output),
 -                    content_type=(
 -                        'application/vnd.oasis.opendocument.spreadsheet'
 -                    )
 -                )
 -                response['Content-Disposition'] = \
 -                        'attachment; filename=Masse Salariale %s.ods' % \
 -                            masse.annee
 -                return response
 -            else:
 -                c['rapport'] = masse.rapport
 -                c['header_keys'] = [h[0] for h in masse.headers]
 -                #on enleve le background pour le header
 -                for h in (
 -                    h for h in masse.headers if 'background-color' in h[2]
 -                ):
 -                    h[2]['background'] = 'none'
 -                h = SortHeaders(request, masse.headers, order_field_type="ot",
 -                        not_sortable=c['header_keys'], order_field="o")
 -                c['headers'] = list(h.headers())
 -                for key, nom, opts in masse.headers:
 -                    c['headers']
 -                c['total'] = masse.grand_totaux[0]
 -                c['total_euro'] = masse.grand_totaux[1]
 -                c['colspan'] = len(c['header_keys']) - 1
 -                get_filtre.append(('ods', True))
 -                query_string = urllib.urlencode(get_filtre)
 -                c['url_ods'] = "%s?%s" % (
 -                        reverse('rhr_masse_salariale'), query_string)
 -
 -    return render(request, 'rh/rapports/masse_salariale.html', c)
 -
 -
 -@login_required
 -@in_one_of_group((groups.grp_correspondants_rh,
 -    groups.grp_administrateurs,
 -    groups.grp_directeurs_bureau,
 -    groups.grp_drh,
 -    groups.grp_drh2))
  def rapports_employes_sans_contrat(request):
 +    """
 +    Employé sans contrat = a un Dossier qui n'a pas de Contrat associé
 +    Employé avec contrat échu = a un Dossier avec un Contrat se terminant hier
 +    au plus tard... (aujourd'hui = ok, pas date de fin = illimité donc ok)
 +    """
      lookup_params = get_lookup_params(request)
  
+     # régionalisation
+     user_groups = request.user.groups.all()
+     q_region = Q()
+     if groups.grp_correspondants_rh in user_groups or\
+        groups.grp_administrateurs in user_groups or\
+        groups.grp_directeurs_bureau in user_groups:
+         employe = get_employe_from_user(request.user)
+         q_region = Q(poste__implantation__region=employe.implantation.region)
      # contrats échus
      contrats_echus = rh.Contrat.objects.filter(
              date_fin__lt=date.today()
              **lookup_params
          )
  
-     # dossiers en cours sans contrat ou contrats échus
-     dossiers = rh.Dossier.objects.filter(
-             Q(date_debut=None) | Q(date_debut__lte=date.today()),
-         ).filter(
-             Q(date_fin=None) | Q(date_fin__gte=date.today()),
+     # dossiers en cours sans contrat
 -    dossiers_sans_contrat = rh.Dossier.objects.filter(q_region & (
++    dossiers = rh.Dossier.objects.filter(q_region & (
+             Q(date_fin=None) | Q(date_fin__gt=date.today())),
          ).exclude(
              date_debut__gt=date.today()
          ).filter(
  
  
  @login_required
- @drh_or_admin_required
++@in_one_of_group((groups.grp_correspondants_rh,
++    groups.grp_administrateurs,
++    groups.grp_directeurs_bureau,
++    groups.grp_drh,
++    groups.grp_drh2))
 +def rapports_masse_salariale(request):
 +    form = MasseSalarialeForm(request.user, request.GET)
 +    if 'annee' in request.GET and form.is_valid():
 +        region = form.cleaned_data['region']
 +        implantation = form.cleaned_data['implantation']
 +        annee = form.cleaned_data['annee']
 +        debut_annee = date(annee, 1, 1)
 +        fin_annee = date(annee, 12, 31)
 +        jours_annee = (fin_annee - debut_annee).days + 1
 +        today = date.today()
 +
 +        # Récupérer les dossiers actifs
 +        dossiers = rh.Dossier.objects \
 +                .actifs(annee=annee) \
 +                .select_related(
 +                    'poste', 'poste__implantation',
 +                    'poste__implantation__region',
 +                    'poste__implantation__adresse_physique_pays',
 +                    'employe', 'poste__type_poste', 'classement',
 +                    'statut', 'organisme_bstg'
 +                ) \
 +                .extra(
 +                    select={
 +                        'valeur_point': (
 +                            'SELECT valeur FROM rh_valeurpoint '
 +                            'WHERE annee = %d '
 +                            'AND implantation = ref_implantation.id' % annee
 +                        ),
 +                        'valeur_point_devise': (
 +                            'SELECT d.code '
 +                            'FROM rh_valeurpoint vp '
 +                            'INNER JOIN rh_devise d ON d.id = vp.devise '
 +                            'WHERE annee = %d '
 +                            'AND implantation = ref_implantation.id' % annee
 +                        )
 +                    }
 +                )
 +        if region:
 +            dossiers = dossiers.filter(poste__implantation__region=region)
 +        if implantation:
 +            dossiers = dossiers.filter(poste__implantation=implantation)
 +
 +        # Récupérer les rémunérations actives
 +        remuns = rh.Remuneration.objects \
 +                .actifs(annee=annee) \
 +                .select_related('devise', 'type') \
 +                .extra(
 +                    tables=['rh_tauxchange'],
 +                    where=[
 +                        'rh_tauxchange.annee = %s',
 +                        'rh_tauxchange.devise = rh_devise.id'
 +                    ],
 +                    params=[annee],
 +                    select={
 +                        'taux_change': 'rh_tauxchange.taux'
 +                    }
 +                )
 +        if region:
 +            remuns = remuns.filter(dossier__poste__implantation__region=region)
 +        if implantation:
 +            remuns = remuns.filter(dossier__poste__implantation=implantation)
 +        remuns_par_dossier = defaultdict(list)
 +        types_remun = set()
 +        for remun in remuns:
 +            types_remun.add(remun.type_id)
 +            remuns_par_dossier[remun.dossier_id].append(remun)
 +
 +        # Récupérer les types de rémunération par nature
 +        types_remun_par_nature = defaultdict(list)
 +        for type in rh.TypeRemuneration.objects.all():
 +            if type.id in types_remun:
 +                types_remun_par_nature[type.nature_remuneration].append(type)
 +        titres_traitements = [
 +            t.nom for t in types_remun_par_nature[u'Traitement']
 +        ]
 +        titres_indemnites = [
 +            t.nom for t in types_remun_par_nature[u'Indemnité']
 +        ]
 +        titres_primes = [
 +            t.nom for t in types_remun_par_nature[u'Accessoire']
 +        ]
 +        titres_charges = [
 +            t.nom for t in types_remun_par_nature[u'Charges']
 +        ]
 +
 +        # Boucler sur les dossiers et préprarer les lignes du rapport
 +        lignes = []
 +        masse_salariale_totale = 0
 +        for dossier in dossiers:
 +            debut_cette_annee = \
 +                    max(dossier.date_debut or debut_annee, debut_annee)
 +            fin_cette_annee = \
 +                    min(dossier.date_fin or fin_annee, fin_annee)
 +            jours = (fin_cette_annee - debut_cette_annee).days + 1
 +
 +            remuns = remuns_par_dossier[dossier.id]
 +            devises = set(remun.devise.code for remun in remuns)
 +            if len(devises) == 1:
 +                devise = remuns[0].devise.code
 +                montant_remun = lambda r: r.montant
 +                taux_change = Decimal(str(remuns[0].taux_change))
 +            else:
 +                devise = 'EUR'
 +                montant_remun = lambda r: (
 +                    r.montant * Decimal(str(r.taux_change))
 +                )
 +                taux_change = Decimal(1)
 +
 +            remuns_par_type = defaultdict(lambda: 0)
 +            for remun in remuns:
 +                if remun.type.nature_remuneration == u'Accessoire':
 +                    remuns_par_type[remun.type_id] += montant_remun(remun)
 +                else:
 +                    remuns_par_type[remun.type_id] += (
 +                        montant_remun(remun) * ((
 +                            min(remun.date_fin or fin_annee, fin_annee) -
 +                            max(remun.date_debut or debut_annee, debut_annee)
 +                        ).days + 1) / jours_annee *
 +                        dossier.regime_travail / 100
 +                    ).quantize(TWOPLACES)
 +            traitements = [
 +                remuns_par_type[type.id]
 +                for type in types_remun_par_nature[u'Traitement']
 +            ]
 +            indemnites = [
 +                remuns_par_type[type.id]
 +                for type in types_remun_par_nature[u'Indemnité']
 +            ]
 +            primes = [
 +                remuns_par_type[type.id]
 +                for type in types_remun_par_nature[u'Accessoire']
 +            ]
 +            charges = [
 +                remuns_par_type[type.id]
 +                for type in types_remun_par_nature[u'Charges']
 +            ]
 +            masse_salariale = sum(remuns_par_type.values())
 +            masse_salariale_eur = (
 +                masse_salariale * taux_change
 +            ).quantize(TWOPLACES)
 +            masse_salariale_totale += masse_salariale_eur
 +
 +            if dossier.valeur_point and dossier.classement \
 +               and dossier.classement.coefficient and dossier.regime_travail:
 +                salaire_theorique = (Decimal(str(
 +                    dossier.valeur_point * dossier.classement.coefficient
 +                )) * dossier.regime_travail / 100).quantize(TWOPLACES)
 +            else:
 +                salaire_theorique = None
 +
 +            lignes.append({
 +                'dossier': dossier,
 +                'poste': dossier.poste,
 +                'date_debut': (
 +                    dossier.date_debut
 +                    if dossier.date_debut and dossier.date_debut.year == annee
 +                    else None
 +                ),
 +                'date_fin': (
 +                    dossier.date_fin
 +                    if dossier.date_fin and dossier.date_fin.year == annee
 +                    else None
 +                ),
 +                'jours': jours,
 +                'devise': devise,
 +                'valeur_point': dossier.valeur_point,
 +                'valeur_point_devise': dossier.valeur_point_devise,
 +                'regime_travail': dossier.regime_travail,
 +                'local_expatrie': {
 +                    'local': 'L', 'expat': 'E'
 +                }.get(dossier.statut_residence),
 +                'salaire_bstg': remuns_par_type[2],
 +                'salaire_bstg_eur': (
 +                    (remuns_par_type[2] * taux_change).quantize(TWOPLACES)
 +                ),
 +                'salaire_theorique': salaire_theorique,
 +                'traitements': traitements,
 +                'total_traitements': sum(traitements),
 +                'indemnites': indemnites,
 +                'total_indemnites': sum(indemnites),
 +                'primes': primes,
 +                'total_primes': sum(primes),
 +                'charges': charges,
 +                'total_charges': sum(charges),
 +                'masse_salariale': masse_salariale,
 +                'masse_salariale_eur': masse_salariale_eur,
 +            })
 +
 +        # Récupérer les postes actifs pour déterminer le nombre de jours
 +        # vacants.
 +        postes = rh.Poste.objects.actifs(annee=annee) \
 +                .select_related(
 +                    'devise_max', 'valeur_point_max', 'classement_max'
 +                ) \
 +                .extra(
 +                    tables=['rh_tauxchange'],
 +                    where=[
 +                        'rh_tauxchange.annee = %s',
 +                        'rh_tauxchange.devise = rh_devise.id'
 +                    ],
 +                    params=[annee],
 +                    select={
 +                        'taux_change': 'rh_tauxchange.taux'
 +                    }
 +                )
 +        if region:
 +            postes = postes.filter(implantation__region=region)
 +        if implantation:
 +            postes = postes.filter(implantation=implantation)
 +        postes = list(postes)
 +        postes_par_id = dict((poste.id, poste) for poste in postes)
 +        jours_vacants_date = dict(
 +            (poste.id, max(today, poste.date_debut or today))
 +            for poste in postes
 +        )
 +        jours_vacants = defaultdict(lambda: 0)
 +        for dossier in rh.Dossier.objects.actifs(annee=annee) \
 +                       .order_by('date_debut'):
 +            if dossier.poste_id not in jours_vacants_date:
 +                continue
 +            derniere_date = jours_vacants_date[dossier.poste_id]
 +            if dossier.date_debut is not None:
 +                jours_vacants[dossier.poste_id] += max((
 +                    dossier.date_debut - derniere_date
 +                ).days - 1, 0)
 +            jours_vacants_date[dossier.poste_id] = max(
 +                min(dossier.date_fin or fin_annee, fin_annee),
 +                derniere_date
 +            )
 +        for poste_id, derniere_date in jours_vacants_date.iteritems():
 +            jours_vacants[poste_id] += max((
 +                min(postes_par_id[poste_id].date_fin or fin_annee, fin_annee) -
 +                derniere_date
 +            ).days, 0)
 +
 +        # Ajouter les lignes des postes vacants au rapport
 +        for poste_id, jours in jours_vacants.iteritems():
 +            if jours == 0:
 +                continue
 +            poste = postes_par_id[poste_id]
 +            if poste.valeur_point_max and poste.classement_max \
 +               and poste.classement_max.coefficient and poste.regime_travail:
 +                salaire_theorique = (Decimal(str(
 +                    poste.valeur_point_max.valeur *
 +                    poste.classement_max.coefficient
 +                )) * poste.regime_travail / 100).quantize(TWOPLACES)
 +            else:
 +                salaire_theorique = None
 +
 +            local_expatrie = '/'.join(
 +                (['L'] if poste.local else []) +
 +                (['E'] if poste.expatrie else [])
 +            )
 +
 +            salaire = (
 +                poste.salaire_max * jours / jours_annee *
 +                poste.regime_travail / 100
 +            ).quantize(TWOPLACES)
 +            indemnites = (
 +                poste.indemn_max * jours / jours_annee *
 +                poste.regime_travail / 100
 +            ).quantize(TWOPLACES)
 +            charges = (
 +                poste.autre_max * jours / jours_annee *
 +                poste.regime_travail / 100
 +            ).quantize(TWOPLACES)
 +            masse_salariale = salaire + indemnites + charges
 +            masse_salariale_eur = (
 +                masse_salariale * Decimal(str(poste.taux_change))
 +            ).quantize(TWOPLACES)
 +            masse_salariale_totale += masse_salariale_eur
 +
 +            lignes.append({
 +                'poste': poste,
 +                'regime_travail': poste.regime_travail,
 +                'local_expatrie': local_expatrie,
 +                'jours': jours,
 +                'devise': poste.devise_max and poste.devise_max.code,
 +                'valeur_point': (
 +                    poste.valeur_point_max and poste.valeur_point_max.valeur
 +                ),
 +                'valeur_point_devise': (
 +                    poste.valeur_point_max and \
 +                    poste.valeur_point_max.devise.code
 +                ),
 +                'salaire_theorique': salaire_theorique,
 +                'traitements': [0] * len(titres_traitements),
 +                'total_traitements': salaire,
 +                'indemnites': [0] * len(titres_indemnites),
 +                'total_indemnites': indemnites,
 +                'primes': [0] * len(titres_primes),
 +                'total_primes': 0,
 +                'charges': [0] * len(titres_charges),
 +                'total_charges': charges,
 +                'masse_salariale': masse_salariale,
 +                'masse_salariale_eur': masse_salariale_eur
 +            })
 +        if 'ods' in request.GET:
 +            doc = ods.masse_salariale(
 +                lignes=lignes,
 +                annee=annee,
 +                titres_traitements=titres_traitements,
 +                titres_indemnites=titres_indemnites,
 +                titres_primes=titres_primes,
 +                titres_charges=titres_charges,
 +                masse_salariale_totale=masse_salariale_totale
 +            )
 +            response = HttpResponse(
 +                mimetype='vnd.oasis.opendocument.spreadsheet'
 +            )
 +            response['Content-Disposition'] = 'filename=masse-salariale.ods'
 +            doc.write(response)
 +            return response
 +        else:
 +            return render(request, 'rh/rapports/masse_salariale.html', {
 +                'form': form,
 +                'titres_traitements': titres_traitements,
 +                'titres_indemnites': titres_indemnites,
 +                'titres_primes': titres_primes,
 +                'titres_charges': titres_charges,
 +                'masse_salariale_totale': masse_salariale_totale,
 +                'lignes': lignes,
 +                'annee': annee
 +            })
 +    return render(request, 'rh/rapports/masse_salariale.html', {
 +        'form': form
 +    })
 +
 +
 +@login_required
  @drh_or_admin_required
  def rapports_postes_modelisation(request):
      c = {}
@@@ -633,7 -446,7 +665,11 @@@ def poste_apercu(request, poste_id)
      }
      return render(request, 'admin/rh/poste/apercu.html', c)
  
 -@region_protected(rh.Employe)
++@in_one_of_group((groups.grp_correspondants_rh,
++    groups.grp_administrateurs,
++    groups.grp_directeurs_bureau,
++    groups.grp_drh,
++    groups.grp_drh2))
  def employe_apercu(request, employe_id):
      employe = get_object_or_404(rh.Employe, pk=employe_id)
      user_groups = request.user.groups.all()
  
  
  @login_required
- @drh_or_admin_required
+ @in_one_of_group((groups.grp_correspondants_rh,
+     groups.grp_administrateurs,
+     groups.grp_directeurs_bureau,
+     groups.grp_drh,
+     groups.grp_drh2))
  def organigrammes_employe(request, id, level="all"):
 -
      poste = get_object_or_404(rh.Poste, pk=id)
      dossiers_by_poste = dict(
          (d.poste_id, d)
                          Q(date_fin__gt=date.today()) | Q(date_fin=None),
                          Q(date_debut__lt=date.today()) | Q(date_debut=None),
                          responsable__in=postes_handle
--                    ).exclude(supprime=True).exclude(responsable=None).all()
++                    ).exclude(responsable=None).all()
  
              for p in postes_handle:
                  if p.responsable_id != p.id:
  
  
  @login_required
- @drh_or_admin_required
+ @in_one_of_group((groups.grp_correspondants_rh,
+     groups.grp_administrateurs,
+     groups.grp_directeurs_bureau,
+     groups.grp_drh,
+     groups.grp_drh2))
  def organigrammes_service(request, id):
 -
      service = get_object_or_404(rh.Service, pk=id)
      svg = rh_graph.organigramme_postes_cluster( \
+             request.user, \
              cluster_filter={"service": service}, \
              titre=u"Organigramme du service %s" % service.nom,
              cluster_titre=service.nom)
  
  
  @login_required
- @drh_or_admin_required
+ @in_one_of_group((groups.grp_correspondants_rh,
+     groups.grp_administrateurs,
+     groups.grp_directeurs_bureau,
+     groups.grp_drh,
+     groups.grp_drh2))
  def organigrammes_implantation(request, id):
 -
      implantation = get_object_or_404(ref.Implantation, pk=id)
      svg = rh_graph.organigramme_postes_cluster( \
+             request.user, \
              cluster_filter={"implantation": implantation}, \
              titre=u"Organigramme de l'implantation %s" % implantation.nom,
              cluster_titre=implantation.nom)
  
  
  @login_required
- @drh_or_admin_required
+ @in_one_of_group((groups.grp_correspondants_rh,
+     groups.grp_administrateurs,
+     groups.grp_directeurs_bureau,
+     groups.grp_drh,
+     groups.grp_drh2))
  def organigrammes_region(request, id):
 -
      region = get_object_or_404(ref.Region, pk=id)
      svg = rh_graph.organigramme_postes_cluster( \
+             request.user, \
              cluster_filter={"implantation__region": region}, \
              titre=u"Organigramme du bureau de %s" % region.nom,
              cluster_titre=region.nom)
Simple merge
          </li>
          {% endif %}
          <li class="{% menu_actif request '^embauches$' %}">
 -          <a href="{% url dae_embauches_liste %}">Embauches : voir et valider</a>
 +          <a href="{% url dae_embauches_liste %}">Personnel : voir et valider</a>
          </li>
          <li class="{% menu_actif request '^embauches_finalisees$' %}">
 -          <a href="{% url embauches_finalisees %}">Embauches finalisées</a>
 +          <a href="{% url embauches_finalisees %}">DAE finalisées</a>
          </li>
-     </ul>
+       </ul>
 -    </li>
 -    {% endif %}
 +  </li>
 +  {% endif %}
+     
 -    {% if perms.rh %}
 -      <li>
 -          <a href="{% url admin:app_list app_label="rh" %}">Gestion des personnels</a>
 -      </li>
 -    {% endif %}
 +  {% if perms.rh %}
 +  <li>
 +    <a href="{% url admin:app_list app_label="rh" %}">Gestion des personnels</a>
 +  </li>
    {% endif %}
  </ul>
diff --cc src/auf.django.metadata/auf/django/metadata/managers.py
index 9e72c29,9e72c29..0000000
deleted file mode 100755,100755
+++ /dev/null
@@@ -1,19 -1,19 +1,0 @@@
--# -*- encoding: utf-8 -*-
--
--from django.db import models
--
--
--class NoDeleteQuerySet(models.query.QuerySet):
--    """
--    Pas de delete, flag à supprimer sur les entrées.
--    """
--    def delete(self):
--        self.update(supprime=True)
--
--
--class NoDeleteManager(models.Manager):
--    """
--    Les entrées supprimées sont exclues des querysets.
--    """
--    def get_query_set(self):
--        return NoDeleteQuerySet(self.model, using=self._db).filter(supprime=False)