diff --git a/kfet/urls.py b/kfet/urls.py index d16aba84..439be38c 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -238,12 +238,7 @@ urlpatterns = [ # ----- # Sales history urls # ----- - path("purchases", views.purchases_csv, name="kfet.purchases"), - path( - "purchases/-/-", - views.purchases_csv, - name="kfet.purchases", - ), + path("purchases", views.SalesStatList.as_view(), name="kfet.purchases"), # ----- # JSON urls # ----- diff --git a/kfet/views.py b/kfet/views.py index 48d75db7..1ac394ad 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2,7 +2,7 @@ import csv import heapq import statistics from collections import defaultdict -from datetime import date, datetime, timedelta +from datetime import datetime, timedelta from decimal import Decimal from typing import List from urllib.parse import urlencode @@ -13,12 +13,13 @@ from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.models import Permission, User from django.contrib.messages.views import SuccessMessageMixin -from django.core.exceptions import SuspiciousOperation, ValidationError +from django.core.exceptions import SuspiciousOperation from django.db import transaction from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery, Sum from django.forms import ValidationError, formset_factory from django.http import ( Http404, + HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, JsonResponse, @@ -30,6 +31,7 @@ from django.utils.decorators import method_decorator from django.views.generic import DetailView, FormView, ListView, TemplateView from django.views.generic.detail import BaseDetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView +from django.views.generic.list import BaseListView from gestioncof.models import CofProfile from kfet import KFET_DELETED_TRIGRAMME, consumers @@ -2456,81 +2458,61 @@ class ScaleMixin(object): # Export des ventes en CSV # ------------------------ +MONTHS = ( + "_Janvier_Février_Mars_Avril_Mai_Juin_Juillet_Août_Septembre_Octobre" + "_Novembre_Décembre" +).split("_") -@teamkfet_required -def purchases_csv( - request, start_year=None, start_month=None, end_year=None, end_month=None -): - """ - Renvoie un historique des achats en K-Fêt, par mois et sur une période donnée - (Par défault les 12 mois précédents) - """ - # Si on ne spécifie pas tout l'intervalle, on prend les 12 derniers mois - if (start_year and start_month and end_year and end_month) is None: - # On utilise replace pour gérer le cas des années bissextiles sans - # changer de mois - end = date.today().replace(day=1) - start = end - timedelta(days=365) - start_year = start.year - start_month = start.month - end_year = end.year - end_month = end.month - # On calcule la liste des premiers jours des mois dont on veut avoir les - # ventes - dates = [] +@method_decorator(teamkfet_required, name="dispatch") +class SalesStatList(BaseListView): + model = Operation - for year in range(start_year, end_year + 1): - month_start = start_month if year == start_year else 1 - month_end = end_month if year == end_year else 12 - for month in range(month_start, month_end + 1): - dates.append(date(year, month, 1)) + def get_queryset(self): + return super().get_queryset().filter(type=Operation.PURCHASE, canceled_at=None) - # On rajoute le mois suivant - dates.append((dates[-1] + timedelta(days=31)).replace(day=1)) - - # On récupère les données voulues - articles_list = list(Article.objects.order_by("name")) - nb_articles = len(articles_list) - articles = {} - for i in range(nb_articles): - articles[articles_list[i].name] = i + 1 - - ventes = ( - Operation.objects.filter( - type="purchase", canceled_by=None, group__at__range=[dates[0], dates[-1]] + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + scale = MonthScale(n_steps=12, last=True) + qs = self.get_queryset() + articles = list(Article.objects.order_by("name")) + indexes = {a.name: index for index, a in enumerate(articles)} + ventes = scale.chunkify_qs( + qs.values_list( + "article__name", "group__at__month", "group__at__year" + ).annotate(Sum("article_nb")), + field="group__at", ) - .exclude(article=None) - .order_by("group__at") - .select_related("group", "article") - ) - # On crée le fichier CSV - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = 'attachment; filename="historique_ventes.csv"' - writer = csv.writer(response) + ctx["header"] = ["Mois"] + [a.name for a in articles] + ctx["ventes"] = [] + ctx["total"] = [0] * len(articles) + for v in ventes: + m_ventes = [0] * len(articles) + for (a_name, month, year, nb_ventes) in v: + index = indexes[a_name] + m_ventes[index] = nb_ventes + ctx["total"][index] += nb_ventes - # On met un article par colonne - header = ["Mois"] + [a for a in articles] - writer.writerow(header) + ctx["ventes"].append(["{} {}".format(MONTHS[month], year)] + m_ventes) - # On calcule la consommation par mois sur la période donnée - i_m = 0 - row = [f"{dates[i_m].month:02d}/{dates[i_m].year}"] + [0] * nb_articles - last_row = ["Total"] + [0] * nb_articles - for v in ventes: - while v.group.at.date() > dates[i_m + 1]: - writer.writerow(row) - i_m += 1 - row = [f"{dates[i_m].month:02d}/{dates[i_m].year}"] + [0] * nb_articles + return ctx - row[articles[v.article.name]] += v.article_nb - last_row[articles[v.article.name]] += v.article_nb + def render_to_response(self, context): + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="historique_ventes.csv"' - writer.writerow(row) - writer.writerow(last_row) + writer = csv.writer(response) + writer.writerow(context["header"]) - return response + # On écrit chaque mois + for line in context["ventes"]: + writer.writerow(line) + + writer.writerow([]) + writer.writerow(["Total"] + context["total"]) + + return response # -----------------------