From ca5e9484778f6cdcb1ca1e306834b7586d97f305 Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Tue, 26 Apr 2022 20:08:13 +0200 Subject: [PATCH] Add reports by hour --- .dockerignore | 1 + docker/run.sh | 1 + src/common/locale/en/LC_MESSAGES/django.po | 2 +- src/common/locale/fr/LC_MESSAGES/django.po | 2 +- src/purchase/locale/en/LC_MESSAGES/django.po | 46 ++++++++++------- src/purchase/locale/fr/LC_MESSAGES/django.po | 46 ++++++++++------- src/purchase/templates/purchase/reports.html | 2 + src/purchase/views/reports.py | 53 +++++++++++++++++++- 8 files changed, 114 insertions(+), 39 deletions(-) diff --git a/.dockerignore b/.dockerignore index 583b32f..73573e6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ __pycache__/ **/__pycache__/ .pytest_cache/ .idea/ +*.mo diff --git a/docker/run.sh b/docker/run.sh index 1b1e9ef..f36cec1 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -1,4 +1,5 @@ #!/bin/bash set -eux python manage.py migrate --noinput +inv compilemessages gunicorn checkout.wsgi -b 0.0.0.0:8000 --log-file - diff --git a/src/common/locale/en/LC_MESSAGES/django.po b/src/common/locale/en/LC_MESSAGES/django.po index b6675a8..326440b 100644 --- a/src/common/locale/en/LC_MESSAGES/django.po +++ b/src/common/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-26 18:17+0200\n" +"POT-Creation-Date: 2022-04-26 20:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/src/common/locale/fr/LC_MESSAGES/django.po b/src/common/locale/fr/LC_MESSAGES/django.po index 8851c35..52ac305 100644 --- a/src/common/locale/fr/LC_MESSAGES/django.po +++ b/src/common/locale/fr/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-26 18:17+0200\n" +"POT-Creation-Date: 2022-04-26 20:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/src/purchase/locale/en/LC_MESSAGES/django.po b/src/purchase/locale/en/LC_MESSAGES/django.po index dd8aa4d..f829076 100644 --- a/src/purchase/locale/en/LC_MESSAGES/django.po +++ b/src/purchase/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-26 18:17+0200\n" +"POT-Creation-Date: 2022-04-26 20:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -46,11 +46,11 @@ msgstr "" msgid "updated at" msgstr "" -#: purchase/models.py:37 purchase/models.py:66 +#: purchase/models.py:37 purchase/models.py:69 msgid "name" msgstr "" -#: purchase/models.py:42 purchase/models.py:149 +#: purchase/models.py:42 purchase/models.py:152 msgid "payment method" msgstr "" @@ -58,52 +58,52 @@ msgstr "" msgid "payment methods" msgstr "" -#: purchase/models.py:67 +#: purchase/models.py:70 msgid "image" msgstr "" -#: purchase/models.py:69 +#: purchase/models.py:72 msgid "unit price (cents)" msgstr "" -#: purchase/models.py:69 +#: purchase/models.py:72 msgid "unit price in cents" msgstr "" -#: purchase/models.py:72 +#: purchase/models.py:75 msgid "display order" msgstr "" -#: purchase/models.py:79 purchase/models.py:177 +#: purchase/models.py:82 purchase/models.py:180 msgid "product" msgstr "" -#: purchase/models.py:80 +#: purchase/models.py:83 msgid "products" msgstr "" -#: purchase/models.py:155 purchase/models.py:183 +#: purchase/models.py:158 purchase/models.py:186 msgid "basket" msgstr "" -#: purchase/models.py:156 +#: purchase/models.py:159 msgid "baskets" msgstr "" -#: purchase/models.py:159 +#: purchase/models.py:162 #, python-format msgid "Basket #%(id)s" msgstr "" -#: purchase/models.py:185 +#: purchase/models.py:188 msgid "quantity" msgstr "" -#: purchase/models.py:190 +#: purchase/models.py:193 msgid "basket item" msgstr "" -#: purchase/models.py:191 +#: purchase/models.py:194 msgid "basket items" msgstr "" @@ -163,6 +163,7 @@ msgstr "" #: purchase/templates/purchase/reports.html:14 #: purchase/templates/purchase/snippets/report_payment_methods.html:8 #: purchase/templates/purchase/snippets/report_products.html:8 +#: purchase/views/reports.py:80 msgid "Turnover" msgstr "" @@ -170,15 +171,15 @@ msgstr "" msgid "Average basket" msgstr "" -#: purchase/templates/purchase/reports.html:27 +#: purchase/templates/purchase/reports.html:29 msgid "Products" msgstr "" -#: purchase/templates/purchase/reports.html:30 +#: purchase/templates/purchase/reports.html:34 msgid "Turnover by payment method" msgstr "" -#: purchase/templates/purchase/reports.html:33 +#: purchase/templates/purchase/reports.html:37 msgid "Baskets without payment method" msgstr "" @@ -203,6 +204,7 @@ msgid "Product" msgstr "" #: purchase/templates/purchase/snippets/report_products.html:7 +#: purchase/views/reports.py:64 msgid "# sold" msgstr "" @@ -217,3 +219,11 @@ msgstr "" #: purchase/views/basket.py:45 msgid "Basket successfully deleted." msgstr "" + +#: purchase/views/reports.py:112 +msgid "Basket count by hour" +msgstr "" + +#: purchase/views/reports.py:117 +msgid "Turnover by hour" +msgstr "" diff --git a/src/purchase/locale/fr/LC_MESSAGES/django.po b/src/purchase/locale/fr/LC_MESSAGES/django.po index 5469a11..722982f 100644 --- a/src/purchase/locale/fr/LC_MESSAGES/django.po +++ b/src/purchase/locale/fr/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-26 18:17+0200\n" +"POT-Creation-Date: 2022-04-26 20:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -43,11 +43,11 @@ msgstr "créé à" msgid "updated at" msgstr "mis à jour à" -#: purchase/models.py:37 purchase/models.py:66 +#: purchase/models.py:37 purchase/models.py:69 msgid "name" msgstr "nom" -#: purchase/models.py:42 purchase/models.py:149 +#: purchase/models.py:42 purchase/models.py:152 msgid "payment method" msgstr "moyen de paiement" @@ -55,52 +55,52 @@ msgstr "moyen de paiement" msgid "payment methods" msgstr "moyens de paiement" -#: purchase/models.py:67 +#: purchase/models.py:70 msgid "image" msgstr "image" -#: purchase/models.py:69 +#: purchase/models.py:72 msgid "unit price (cents)" msgstr "prix unitaire (centimes)" -#: purchase/models.py:69 +#: purchase/models.py:72 msgid "unit price in cents" msgstr "prix unitaire en centimes" -#: purchase/models.py:72 +#: purchase/models.py:75 msgid "display order" msgstr "ordre d'affichage" -#: purchase/models.py:79 purchase/models.py:177 +#: purchase/models.py:82 purchase/models.py:180 msgid "product" msgstr "produit" -#: purchase/models.py:80 +#: purchase/models.py:83 msgid "products" msgstr "produits" -#: purchase/models.py:155 purchase/models.py:183 +#: purchase/models.py:158 purchase/models.py:186 msgid "basket" msgstr "panier" -#: purchase/models.py:156 +#: purchase/models.py:159 msgid "baskets" msgstr "paniers" -#: purchase/models.py:159 +#: purchase/models.py:162 #, python-format msgid "Basket #%(id)s" msgstr "Panier n°%(id)s" -#: purchase/models.py:185 +#: purchase/models.py:188 msgid "quantity" msgstr "quantité" -#: purchase/models.py:190 +#: purchase/models.py:193 msgid "basket item" msgstr "article de panier" -#: purchase/models.py:191 +#: purchase/models.py:194 msgid "basket items" msgstr "articles de panier" @@ -160,6 +160,7 @@ msgstr "Par jour" #: purchase/templates/purchase/reports.html:14 #: purchase/templates/purchase/snippets/report_payment_methods.html:8 #: purchase/templates/purchase/snippets/report_products.html:8 +#: purchase/views/reports.py:80 msgid "Turnover" msgstr "Chiffre d'affaires" @@ -167,15 +168,15 @@ msgstr "Chiffre d'affaires" msgid "Average basket" msgstr "Panier moyen" -#: purchase/templates/purchase/reports.html:27 +#: purchase/templates/purchase/reports.html:29 msgid "Products" msgstr "Produits" -#: purchase/templates/purchase/reports.html:30 +#: purchase/templates/purchase/reports.html:34 msgid "Turnover by payment method" msgstr "Chiffre d'affaires par moyen de paiement" -#: purchase/templates/purchase/reports.html:33 +#: purchase/templates/purchase/reports.html:37 msgid "Baskets without payment method" msgstr "Paniers sans moyen de paiement" @@ -200,6 +201,7 @@ msgid "Product" msgstr "Produit" #: purchase/templates/purchase/snippets/report_products.html:7 +#: purchase/views/reports.py:64 msgid "# sold" msgstr "Nb. vendus" @@ -215,6 +217,14 @@ msgstr "Panier correctement modifié." msgid "Basket successfully deleted." msgstr "Panier correctement supprimé." +#: purchase/views/reports.py:112 +msgid "Basket count by hour" +msgstr "Nombre de paniers par heure" + +#: purchase/views/reports.py:117 +msgid "Turnover by hour" +msgstr "Chiffre d'affaires par heure" + #, python-format #~ msgid "Total turnover: %(total|currency)s" #~ msgstr "Chiffre d'affaires total : %(total|currency)s" diff --git a/src/purchase/templates/purchase/reports.html b/src/purchase/templates/purchase/reports.html index de895f7..7af90cb 100644 --- a/src/purchase/templates/purchase/reports.html +++ b/src/purchase/templates/purchase/reports.html @@ -24,6 +24,8 @@ {% endfor %} + {{ by_hour_plot|safe }} +

{% translate "Products" %}

{% include "purchase/snippets/report_products.html" %} {{ products_sold_plot|safe }} diff --git a/src/purchase/views/reports.py b/src/purchase/views/reports.py index 01ab47b..76d8b70 100644 --- a/src/purchase/views/reports.py +++ b/src/purchase/views/reports.py @@ -1,3 +1,4 @@ +import datetime from io import StringIO from django.utils.translation import gettext as _ @@ -5,7 +6,13 @@ from django.views.generic import TemplateView from matplotlib import pyplot as plt from matplotlib import ticker -from purchase.models import Basket, PaymentMethod, Product, ProductQuerySet +from purchase.models import ( + Basket, + BasketQuerySet, + PaymentMethod, + Product, + ProductQuerySet, +) from purchase.views.utils import ProtectedViewsMixin @@ -26,6 +33,8 @@ class ReportsView(ProtectedViewsMixin, TemplateView): products = Product.objects.with_turnover().with_sold() products_sold_plot = self.get_products_sold_plot(products) products_turnover_plot = self.get_products_turnover_plot(products) + baskets = list(Basket.objects.priced().order_by("created_at")) + by_hour_plot = self.by_hour_plot(baskets) context.update( { "turnover": Basket.objects.turnover(), @@ -35,6 +44,7 @@ class ReportsView(ProtectedViewsMixin, TemplateView): "products": products, "products_sold_plot": products_sold_plot, "products_turnover_plot": products_turnover_plot, + "by_hour_plot": by_hour_plot, "payment_methods": PaymentMethod.objects.with_turnover().with_sold(), "no_payment_method": Basket.objects.no_payment_method().priced(), } @@ -71,3 +81,44 @@ class ReportsView(ProtectedViewsMixin, TemplateView): fig.savefig(image_data, format="svg") image_data.seek(0) return image_data.getvalue() + + def by_hour_plot(self, baskets): + current: datetime.datetime = baskets[0].created_at + current = current.replace(minute=0, second=0, microsecond=0) + end: datetime.datetime = baskets[-1].created_at + basket_index = 0 + labels = [] + counts = [] + turnovers = [] + while current < end: + end_slot = current + datetime.timedelta(hours=1) + basket = baskets[basket_index] + count = 0 + turnover = 0 + while basket.created_at < end_slot and basket_index < len(baskets) - 1: + count += 1 + turnover += basket.price / 100 + basket_index += 1 + basket = baskets[basket_index] + labels.append(current) + counts.append(count) + turnovers.append(turnover) + current = end_slot + fig, ax1 = plt.subplots() + hours_in_day = 24 + color = "tab:orange" + ax1.bar(labels, counts, width=1 / hours_in_day, color=color) + ax1.tick_params(axis="x", rotation=15) + ax1.set_ylabel(_("Basket count by hour"), color=color) + + color = "tab:blue" + ax2 = ax1.twinx() + ax2.bar(labels, turnovers, width=1 / (hours_in_day * 2), color=color) + ax2.set_ylabel(_("Turnover by hour"), color=color) + plt.gca().yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f€")) + + fig.tight_layout() + image_data = StringIO() + fig.savefig(image_data, format="svg") + image_data.seek(0) + return image_data.getvalue()