Moar graphs

This commit is contained in:
Gabriel Augendre 2022-04-27 22:59:52 +02:00
parent 212b0d9713
commit 282318ac81
6 changed files with 138 additions and 58 deletions

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-26 23:30+0200\n" "POT-Creation-Date: 2022-04-27 22:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"

View file

@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-26 23:30+0200\n" "POT-Creation-Date: 2022-04-27 22:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-26 23:30+0200\n" "POT-Creation-Date: 2022-04-27 22:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -50,7 +50,7 @@ msgstr ""
msgid "name" msgid "name"
msgstr "" msgstr ""
#: purchase/models.py:47 purchase/models.py:170 #: purchase/models.py:47 purchase/models.py:167
msgid "payment method" msgid "payment method"
msgstr "" msgstr ""
@ -62,7 +62,7 @@ msgstr ""
msgid "image" msgid "image"
msgstr "" msgstr ""
#: purchase/models.py:86 #: purchase/models.py:86 purchase/models.py:203
msgid "unit price (cents)" msgid "unit price (cents)"
msgstr "" msgstr ""
@ -74,7 +74,7 @@ msgstr ""
msgid "display order" msgid "display order"
msgstr "" msgstr ""
#: purchase/models.py:96 purchase/models.py:198 #: purchase/models.py:96 purchase/models.py:193
msgid "product" msgid "product"
msgstr "" msgstr ""
@ -82,28 +82,32 @@ msgstr ""
msgid "products" msgid "products"
msgstr "" msgstr ""
#: purchase/models.py:176 purchase/models.py:204 #: purchase/models.py:173 purchase/models.py:199
msgid "basket" msgid "basket"
msgstr "" msgstr ""
#: purchase/models.py:177 #: purchase/models.py:174
msgid "baskets" msgid "baskets"
msgstr "" msgstr ""
#: purchase/models.py:180 #: purchase/models.py:177
#, python-format #, python-format
msgid "Basket #%(id)s" msgid "Basket #%(id)s"
msgstr "" msgstr ""
#: purchase/models.py:206 #: purchase/models.py:201
msgid "quantity" msgid "quantity"
msgstr "" msgstr ""
#: purchase/models.py:211 #: purchase/models.py:204
msgid "product's unit price in cents at the time of purchase"
msgstr ""
#: purchase/models.py:210
msgid "basket item" msgid "basket item"
msgstr "" msgstr ""
#: purchase/models.py:212 #: purchase/models.py:211
msgid "basket items" msgid "basket items"
msgstr "" msgstr ""
@ -174,11 +178,11 @@ msgstr ""
msgid "Products" msgid "Products"
msgstr "" msgstr ""
#: purchase/templates/purchase/reports.html:38 #: purchase/templates/purchase/reports.html:40
msgid "Turnover by payment method" msgid "Turnover by payment method"
msgstr "" msgstr ""
#: purchase/templates/purchase/reports.html:41 #: purchase/templates/purchase/reports.html:43
msgid "Baskets without payment method" msgid "Baskets without payment method"
msgstr "" msgstr ""
@ -203,7 +207,7 @@ msgid "Product"
msgstr "" msgstr ""
#: purchase/templates/purchase/snippets/report_products.html:7 #: purchase/templates/purchase/snippets/report_products.html:7
#: purchase/views/reports.py:70 #: purchase/views/reports.py:79 purchase/views/reports.py:93
msgid "# sold" msgid "# sold"
msgstr "" msgstr ""
@ -219,18 +223,26 @@ msgstr ""
msgid "Basket successfully deleted." msgid "Basket successfully deleted."
msgstr "" msgstr ""
#: purchase/views/reports.py:28 #: purchase/views/reports.py:29
msgid "No sale to report" msgid "No sale to report"
msgstr "" msgstr ""
#: purchase/views/reports.py:75 #: purchase/views/reports.py:70
msgid "Sales by product"
msgstr ""
#: purchase/views/reports.py:85 purchase/views/reports.py:100
msgid "Turnover by product" msgid "Turnover by product"
msgstr "" msgstr ""
#: purchase/views/reports.py:113 #: purchase/views/reports.py:122
msgid "Sales by hour"
msgstr ""
#: purchase/views/reports.py:133
msgid "Basket count by hour" msgid "Basket count by hour"
msgstr "" msgstr ""
#: purchase/views/reports.py:118 #: purchase/views/reports.py:141
msgid "Turnover by hour" msgid "Turnover by hour"
msgstr "" msgstr ""

View file

@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-26 23:30+0200\n" "POT-Creation-Date: 2022-04-27 22:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -47,7 +47,7 @@ msgstr "mis à jour à"
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
#: purchase/models.py:47 purchase/models.py:170 #: purchase/models.py:47 purchase/models.py:167
msgid "payment method" msgid "payment method"
msgstr "moyen de paiement" msgstr "moyen de paiement"
@ -59,7 +59,7 @@ msgstr "moyens de paiement"
msgid "image" msgid "image"
msgstr "image" msgstr "image"
#: purchase/models.py:86 #: purchase/models.py:86 purchase/models.py:203
msgid "unit price (cents)" msgid "unit price (cents)"
msgstr "prix unitaire (centimes)" msgstr "prix unitaire (centimes)"
@ -71,7 +71,7 @@ msgstr "prix unitaire en centimes"
msgid "display order" msgid "display order"
msgstr "ordre d'affichage" msgstr "ordre d'affichage"
#: purchase/models.py:96 purchase/models.py:198 #: purchase/models.py:96 purchase/models.py:193
msgid "product" msgid "product"
msgstr "produit" msgstr "produit"
@ -79,28 +79,32 @@ msgstr "produit"
msgid "products" msgid "products"
msgstr "produits" msgstr "produits"
#: purchase/models.py:176 purchase/models.py:204 #: purchase/models.py:173 purchase/models.py:199
msgid "basket" msgid "basket"
msgstr "panier" msgstr "panier"
#: purchase/models.py:177 #: purchase/models.py:174
msgid "baskets" msgid "baskets"
msgstr "paniers" msgstr "paniers"
#: purchase/models.py:180 #: purchase/models.py:177
#, python-format #, python-format
msgid "Basket #%(id)s" msgid "Basket #%(id)s"
msgstr "Panier n°%(id)s" msgstr "Panier n°%(id)s"
#: purchase/models.py:206 #: purchase/models.py:201
msgid "quantity" msgid "quantity"
msgstr "quantité" msgstr "quantité"
#: purchase/models.py:211 #: purchase/models.py:204
msgid "product's unit price in cents at the time of purchase"
msgstr "prix unitaire du produit en centimes au moment de l'achat"
#: purchase/models.py:210
msgid "basket item" msgid "basket item"
msgstr "article de panier" msgstr "article de panier"
#: purchase/models.py:212 #: purchase/models.py:211
msgid "basket items" msgid "basket items"
msgstr "articles de panier" msgstr "articles de panier"
@ -171,11 +175,11 @@ msgstr "Panier moyen"
msgid "Products" msgid "Products"
msgstr "Produits" msgstr "Produits"
#: purchase/templates/purchase/reports.html:38 #: purchase/templates/purchase/reports.html:40
msgid "Turnover by payment method" msgid "Turnover by payment method"
msgstr "Chiffre d'affaires par moyen de paiement" msgstr "Chiffre d'affaires par moyen de paiement"
#: purchase/templates/purchase/reports.html:41 #: purchase/templates/purchase/reports.html:43
msgid "Baskets without payment method" msgid "Baskets without payment method"
msgstr "Paniers sans moyen de paiement" msgstr "Paniers sans moyen de paiement"
@ -200,7 +204,7 @@ msgid "Product"
msgstr "Produit" msgstr "Produit"
#: purchase/templates/purchase/snippets/report_products.html:7 #: purchase/templates/purchase/snippets/report_products.html:7
#: purchase/views/reports.py:70 #: purchase/views/reports.py:79 purchase/views/reports.py:93
msgid "# sold" msgid "# sold"
msgstr "Nb. vendus" msgstr "Nb. vendus"
@ -216,19 +220,27 @@ msgstr "Panier correctement modifié."
msgid "Basket successfully deleted." msgid "Basket successfully deleted."
msgstr "Panier correctement supprimé." msgstr "Panier correctement supprimé."
#: purchase/views/reports.py:28 #: purchase/views/reports.py:29
msgid "No sale to report" msgid "No sale to report"
msgstr "Aucune vente à afficher" msgstr "Aucune vente à afficher"
#: purchase/views/reports.py:75 #: purchase/views/reports.py:70
msgid "Sales by product"
msgstr "Ventes par produit"
#: purchase/views/reports.py:85 purchase/views/reports.py:100
msgid "Turnover by product" msgid "Turnover by product"
msgstr "Chiffre d'affaires par produit" msgstr "Chiffre d'affaires par produit"
#: purchase/views/reports.py:113 #: purchase/views/reports.py:122
msgid "Sales by hour"
msgstr "Ventes par heure"
#: purchase/views/reports.py:133
msgid "Basket count by hour" msgid "Basket count by hour"
msgstr "Nombre de paniers par heure" msgstr "Nombre de paniers par heure"
#: purchase/views/reports.py:118 #: purchase/views/reports.py:141
msgid "Turnover by hour" msgid "Turnover by hour"
msgstr "Chiffre d'affaires par heure" msgstr "Chiffre d'affaires par heure"

View file

@ -34,6 +34,8 @@
<h2>{% translate "Products" %}</h2> <h2>{% translate "Products" %}</h2>
{% include "purchase/snippets/report_products.html" %} {% include "purchase/snippets/report_products.html" %}
{{ products_plot|safe }} {{ products_plot|safe }}
{{ products_sold_pie|safe }}
{{ products_turnover_pie|safe }}
<h2>{% translate "Turnover by payment method" %}</h2> <h2>{% translate "Turnover by payment method" %}</h2>
{% include "purchase/snippets/report_payment_methods.html" %} {% include "purchase/snippets/report_payment_methods.html" %}

View file

@ -1,11 +1,18 @@
import datetime import datetime
from io import StringIO from io import StringIO
from zoneinfo import ZoneInfo
import numpy as np
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from matplotlib import pyplot as plt from matplotlib import pyplot as plt
from matplotlib import ticker from matplotlib import ticker
from matplotlib.axes import Axes
from matplotlib.container import BarContainer
from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, HourLocator
from matplotlib.figure import Figure
from purchase.models import Basket, PaymentMethod, Product, ProductQuerySet from purchase.models import Basket, PaymentMethod, Product, ProductQuerySet
from purchase.views.utils import ProtectedViewsMixin from purchase.views.utils import ProtectedViewsMixin
@ -31,7 +38,11 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
} }
products = Product.objects.with_turnover().with_sold() products = Product.objects.with_turnover().with_sold()
products_plot = self.get_products_plot(products) (
products_plot,
products_sold_pie,
products_turnover_pie,
) = self.get_products_plots(products)
by_hour_plot = self.by_hour_plot(baskets) by_hour_plot = self.by_hour_plot(baskets)
context.update( context.update(
{ {
@ -41,6 +52,8 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
"average_basket_by_day": average_basket_by_day, "average_basket_by_day": average_basket_by_day,
"products": products, "products": products,
"products_plot": products_plot, "products_plot": products_plot,
"products_sold_pie": products_sold_pie,
"products_turnover_pie": products_turnover_pie,
"by_hour_plot": by_hour_plot, "by_hour_plot": by_hour_plot,
"payment_methods": PaymentMethod.objects.with_turnover().with_sold(), "payment_methods": PaymentMethod.objects.with_turnover().with_sold(),
"no_payment_method": Basket.objects.no_payment_method().priced(), "no_payment_method": Basket.objects.no_payment_method().priced(),
@ -48,26 +61,49 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
) )
return context return context
def get_products_plot(self, products: ProductQuerySet): def get_products_plots(self, products: ProductQuerySet):
labels, sold, turnover = self.get_products_data_for_plot(products) labels, sold, turnover = self.get_products_data_for_plot(products)
fig, ax1 = plt.subplots() x = np.arange(len(labels))
width = 0.4
fig: Figure = plt.figure()
fig.suptitle(_("Sales by product"))
color = "tab:orange" color = "tab:orange"
ax1.bar(labels, sold, width=0.8, color=color) ax1: Axes = fig.add_subplot()
bar: BarContainer = ax1.bar(x - width / 2, sold, width=width, color=color)
ax1.bar_label(bar)
ax1.tick_params(axis="x", rotation=15) ax1.tick_params(axis="x", rotation=15)
ax1.tick_params(axis="y", labelcolor=color)
ax1.set_xticks(x, labels)
ax1.set_ylabel(_("# sold"), color=color) ax1.set_ylabel(_("# sold"), color=color)
color = "tab:blue" color = "tab:blue"
ax2 = ax1.twinx() ax2: Axes = ax1.twinx()
ax2.bar(labels, turnover, width=0.4, color=color) bar = ax2.bar(x + width / 2, turnover, width=width, color=color)
ax2.bar_label(bar, fmt="%d")
ax2.set_ylabel(_("Turnover by product"), color=color) ax2.set_ylabel(_("Turnover by product"), color=color)
plt.gca().yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) ax2.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f"))
ax2.tick_params(axis="y", labelcolor=color)
fig.tight_layout() fig.tight_layout()
image_data = StringIO() img1 = self.get_image_from_fig(fig)
fig.savefig(image_data, format="svg")
image_data.seek(0) fig = plt.figure()
return image_data.getvalue() fig.suptitle(_("# sold"))
ax1 = fig.add_subplot()
ax1.pie(sold, labels=labels, autopct="%d%%")
fig.tight_layout()
img2 = self.get_image_from_fig(fig)
fig = plt.figure()
fig.suptitle(_("Turnover by product"))
ax1 = fig.add_subplot()
ax1.pie(turnover, labels=labels, autopct="%d%%")
fig.tight_layout()
img3 = self.get_image_from_fig(fig)
return img1, img2, img3
def get_products_data_for_plot(self, products): def get_products_data_for_plot(self, products):
labels = [] labels = []
@ -80,25 +116,36 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
return labels, sold, turnover return labels, sold, turnover
def by_hour_plot(self, baskets): def by_hour_plot(self, baskets):
counts, labels, turnovers = self.get_by_hour_data_for_plot(baskets) labels, counts, turnovers = self.get_by_hour_data_for_plot(baskets)
fig, ax1 = plt.subplots()
hours_in_day = 24 hours_in_day = 24
fig: Figure = plt.figure()
fig.suptitle(_("Sales by hour"))
ax1: Axes = fig.add_subplot()
color = "tab:orange" color = "tab:orange"
ax1.bar(labels, counts, width=1 / hours_in_day, color=color) ax1.bar(labels, counts, width=1 / hours_in_day, color=color)
ax1.tick_params(axis="x", rotation=15) ax1.tick_params(axis="x", rotation=15)
ax1.xaxis.set_minor_locator(HourLocator())
tz = ZoneInfo(settings.TIME_ZONE)
locator = AutoDateLocator(tz=tz)
ax1.xaxis.set_major_locator(locator)
ax1.xaxis.set_major_formatter(ConciseDateFormatter(locator, tz=tz))
ax1.set_ylabel(_("Basket count by hour"), color=color) ax1.set_ylabel(_("Basket count by hour"), color=color)
ax1.set_yticks(np.linspace(*ax1.get_ybound(), 8))
ax1.tick_params(axis="y", labelcolor=color)
ax1.grid(visible=True, which="major", axis="y")
color = "tab:blue" color = "tab:blue"
ax2 = ax1.twinx() ax2: Axes = ax1.twinx()
ax2.bar(labels, turnovers, width=1 / (hours_in_day * 2), color=color) ax2.plot(labels, turnovers, ".-", color=color)
ax2.set_ylabel(_("Turnover by hour"), color=color) ax2.set_ylabel(_("Turnover by hour"), color=color)
plt.gca().yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) ax2.set_ylim(bottom=0)
ax2.set_yticks(np.linspace(*ax2.get_ybound(), 8))
ax2.tick_params(axis="y", labelcolor=color)
ax2.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f"))
fig.tight_layout() fig.tight_layout()
image_data = StringIO() return self.get_image_from_fig(fig)
fig.savefig(image_data, format="svg")
image_data.seek(0)
return image_data.getvalue()
def get_by_hour_data_for_plot(self, baskets): def get_by_hour_data_for_plot(self, baskets):
current: datetime.datetime = baskets[0].created_at current: datetime.datetime = baskets[0].created_at
@ -120,8 +167,15 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
if basket_index == len(baskets): if basket_index == len(baskets):
break break
basket = baskets[basket_index] basket = baskets[basket_index]
labels.append(current) labels.append(current.astimezone(ZoneInfo(settings.TIME_ZONE)))
counts.append(count) counts.append(count)
turnovers.append(turnover) turnovers.append(turnover)
current = end_slot current = end_slot
return counts, labels, turnovers return labels, counts, turnovers
def get_image_from_fig(self, fig):
image_data = StringIO()
fig.savefig(image_data, format="svg")
image_data.seek(0)
img1 = image_data.getvalue()
return img1