Merge branch 'masse-salariale' into dev
[auf_rh_dae.git] / project / rh / views.py
index f778732..b65eef3 100644 (file)
@@ -1,41 +1,37 @@
 # -*- encoding: utf-8 -*-
 
-import urllib
+from collections import defaultdict
 from datetime import date
-from itertools import izip
-import StringIO
+from decimal import Decimal
 
 import pygraphviz as pgv
-
-from django import forms
+from auf.django.references import models as ref
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
-from django.core.servers.basehttp import FileWrapper
 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 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
-from project.groups import grp_drh, grp_correspondants_rh
-
-from project.rh import models as rh
+from project.groups import \
+        get_employe_from_user, grp_drh, grp_correspondants_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
 
+TWOPLACES = Decimal('0.01')
+
 
 @login_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,
@@ -106,7 +102,6 @@ def rapports_contrat(request):
 
     # affichage
     employes = set([c.dossier.employe_id for c in contrats])
-
     headers = [
         ("dossier__employe__id", u"#"),
         ("dossier__employe__nom", u"Employé"),
@@ -160,12 +155,12 @@ def rapports_employes_sans_contrat(request):
         ).filter(
             # sans contrat | contrat échu
             Q(rh_contrats=None) | Q(rh_contrats__in=contrats_echus)
-        ).distinct()   
-    
+        ).distinct()
+
     # employés sans contrat ou contrats échus
     employes = rh.Employe.objects.filter(rh_dossiers__in=dossiers)  \
         .distinct().count()
-        
+
     # tri
     if 'o' in request.GET:
         dossiers = dossiers.order_by(
@@ -202,110 +197,308 @@ def rapports_employes_sans_contrat(request):
 @login_required
 @drh_or_admin_required
 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(
+                    tables=['rh_valeurpoint', 'rh_devise'],
+                    where=[
+                        'rh_valeurpoint.annee = %s',
+                        'rh_valeurpoint.implantation = ref_implantation.id',
+                        'rh_devise.id = rh_valeurpoint.devise'
+                    ],
+                    params=[annee],
+                    select={
+                        'valeur_point': 'rh_valeurpoint.valeur',
+                        'valeur_point_devise': 'rh_devise.code'
+                    }
+                )
+        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'
+                    }
+                )
+        remuns_par_dossier = defaultdict(list)
+        for remun in remuns:
+            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():
+            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']
+        ]
 
-    class RechercheTemporelle(forms.Form):
-        CHOICE_ANNEES = range(
-            rh.Remuneration.objects.exclude(date_debut=None)
-            .order_by('date_debut')[0].date_debut.year,
-            date.today().year + 1
-        )
+        # 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
+                    ).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,
+            })
 
-        annee = forms.CharField(
-            initial=date.today().year,
-            widget=forms.Select(
-                choices=((a, a) for a in reversed(CHOICE_ANNEES))
+        # Récupérer les postes actifs pour déterminer le nombre de jours
+        # vacants.
+        postes = list(
+            rh.Poste.objects.actifs(annee=annee)
+            .select_related(
+                'devise_max', 'valeur_point_max', 'classement_max'
             )
-        )
-
-        region = forms.CharField(
-                widget=forms.Select(choices=[('', '')] +
-                    [(i.id, i) for i in ref.Region.objects.all()]
-                )
+            .extra(
+                tables=['rh_tauxchange'],
+                where=[
+                    'rh_tauxchange.annee = %s',
+                    'rh_tauxchange.devise = rh_devise.id'
+                ],
+                params=[annee],
+                select={
+                    'taux_change': 'rh_tauxchange.taux'
+                }
             )
-
-        implantation = forms.CharField(
-                widget=forms.Select(choices=[('', '')] +
-                    [(i.id, i) for i in ref.Implantation.objects.all()]
-                )
+        )
+        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
             )
 
-        #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')
+        # 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
 
-    custom_filter = {}
-    if implantation:
-        custom_filter['dossier__poste__implantation'] = implantation
-    if region:
-        custom_filter['dossier__poste__implantation__region'] = region
+            local_expatrie = '/'.join(
+                (['L'] if poste.local else []) +
+                (['E'] if poste.expatrie else [])
+            )
 
-    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)
+            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,
+                'salaire_de_base': salaire,
+                'total_indemnites': indemnites,
+                '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
@@ -372,6 +565,7 @@ def rapports_postes_service(request):
     c['data'] = data
     return render(request, 'rh/rapports/postes_service.html', c)
 
+
 @region_protected(rh.Dossier)
 def dossier_apercu(request, dossier_id):
     d = get_object_or_404(rh.Dossier, pk=dossier_id)
@@ -442,7 +636,6 @@ def employe_apercu(request, employe_id):
 @login_required
 @drh_or_admin_required
 def organigrammes_employe(request, id, level="all"):
-
     poste = get_object_or_404(rh.Poste, pk=id)
     dossiers_by_poste = dict(
         (d.poste_id, d)
@@ -524,7 +717,6 @@ def organigrammes_employe(request, id, level="all"):
 @login_required
 @drh_or_admin_required
 def organigrammes_service(request, id):
-
     service = get_object_or_404(rh.Service, pk=id)
     svg = rh_graph.organigramme_postes_cluster( \
             cluster_filter={"service": service}, \
@@ -535,7 +727,7 @@ def organigrammes_service(request, id):
         'svg': svg
     }
 
-    return render(request, 'rh/organigrammes/vide.html', c, 
+    return render(request, 'rh/organigrammes/vide.html', c,
         content_type="image/svg+xml"
     )
 
@@ -543,7 +735,6 @@ def organigrammes_service(request, id):
 @login_required
 @drh_or_admin_required
 def organigrammes_implantation(request, id):
-
     implantation = get_object_or_404(ref.Implantation, pk=id)
     svg = rh_graph.organigramme_postes_cluster( \
             cluster_filter={"implantation": implantation}, \
@@ -554,7 +745,7 @@ def organigrammes_implantation(request, id):
         'svg': svg
     }
 
-    return render(request, 'rh/organigrammes/vide.html', c, 
+    return render(request, 'rh/organigrammes/vide.html', c,
         content_type="image/svg+xml"
     )
 
@@ -562,7 +753,6 @@ def organigrammes_implantation(request, id):
 @login_required
 @drh_or_admin_required
 def organigrammes_region(request, id):
-
     region = get_object_or_404(ref.Region, pk=id)
     svg = rh_graph.organigramme_postes_cluster( \
             cluster_filter={"implantation__region": region}, \
@@ -573,7 +763,7 @@ def organigrammes_region(request, id):
         'svg': svg
     }
 
-    return render(request, 
-        'rh/organigrammes/vide.html', c, 
+    return render(request,
+        'rh/organigrammes/vide.html', c,
         content_type="image/svg+xml"
     )