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 ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View file

@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -50,7 +50,7 @@ msgstr ""
msgid "name"
msgstr ""
#: purchase/models.py:47 purchase/models.py:170
#: purchase/models.py:47 purchase/models.py:167
msgid "payment method"
msgstr ""
@ -62,7 +62,7 @@ msgstr ""
msgid "image"
msgstr ""
#: purchase/models.py:86
#: purchase/models.py:86 purchase/models.py:203
msgid "unit price (cents)"
msgstr ""
@ -74,7 +74,7 @@ msgstr ""
msgid "display order"
msgstr ""
#: purchase/models.py:96 purchase/models.py:198
#: purchase/models.py:96 purchase/models.py:193
msgid "product"
msgstr ""
@ -82,28 +82,32 @@ msgstr ""
msgid "products"
msgstr ""
#: purchase/models.py:176 purchase/models.py:204
#: purchase/models.py:173 purchase/models.py:199
msgid "basket"
msgstr ""
#: purchase/models.py:177
#: purchase/models.py:174
msgid "baskets"
msgstr ""
#: purchase/models.py:180
#: purchase/models.py:177
#, python-format
msgid "Basket #%(id)s"
msgstr ""
#: purchase/models.py:206
#: purchase/models.py:201
msgid "quantity"
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"
msgstr ""
#: purchase/models.py:212
#: purchase/models.py:211
msgid "basket items"
msgstr ""
@ -174,11 +178,11 @@ msgstr ""
msgid "Products"
msgstr ""
#: purchase/templates/purchase/reports.html:38
#: purchase/templates/purchase/reports.html:40
msgid "Turnover by payment method"
msgstr ""
#: purchase/templates/purchase/reports.html:41
#: purchase/templates/purchase/reports.html:43
msgid "Baskets without payment method"
msgstr ""
@ -203,7 +207,7 @@ msgid "Product"
msgstr ""
#: 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"
msgstr ""
@ -219,18 +223,26 @@ msgstr ""
msgid "Basket successfully deleted."
msgstr ""
#: purchase/views/reports.py:28
#: purchase/views/reports.py:29
msgid "No sale to report"
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"
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"
msgstr ""
#: purchase/views/reports.py:118
#: purchase/views/reports.py:141
msgid "Turnover by hour"
msgstr ""

View file

@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -47,7 +47,7 @@ msgstr "mis à jour à"
msgid "name"
msgstr "nom"
#: purchase/models.py:47 purchase/models.py:170
#: purchase/models.py:47 purchase/models.py:167
msgid "payment method"
msgstr "moyen de paiement"
@ -59,7 +59,7 @@ msgstr "moyens de paiement"
msgid "image"
msgstr "image"
#: purchase/models.py:86
#: purchase/models.py:86 purchase/models.py:203
msgid "unit price (cents)"
msgstr "prix unitaire (centimes)"
@ -71,7 +71,7 @@ msgstr "prix unitaire en centimes"
msgid "display order"
msgstr "ordre d'affichage"
#: purchase/models.py:96 purchase/models.py:198
#: purchase/models.py:96 purchase/models.py:193
msgid "product"
msgstr "produit"
@ -79,28 +79,32 @@ msgstr "produit"
msgid "products"
msgstr "produits"
#: purchase/models.py:176 purchase/models.py:204
#: purchase/models.py:173 purchase/models.py:199
msgid "basket"
msgstr "panier"
#: purchase/models.py:177
#: purchase/models.py:174
msgid "baskets"
msgstr "paniers"
#: purchase/models.py:180
#: purchase/models.py:177
#, python-format
msgid "Basket #%(id)s"
msgstr "Panier n°%(id)s"
#: purchase/models.py:206
#: purchase/models.py:201
msgid "quantity"
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"
msgstr "article de panier"
#: purchase/models.py:212
#: purchase/models.py:211
msgid "basket items"
msgstr "articles de panier"
@ -171,11 +175,11 @@ msgstr "Panier moyen"
msgid "Products"
msgstr "Produits"
#: purchase/templates/purchase/reports.html:38
#: purchase/templates/purchase/reports.html:40
msgid "Turnover by payment method"
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"
msgstr "Paniers sans moyen de paiement"
@ -200,7 +204,7 @@ msgid "Product"
msgstr "Produit"
#: 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"
msgstr "Nb. vendus"
@ -216,19 +220,27 @@ msgstr "Panier correctement modifié."
msgid "Basket successfully deleted."
msgstr "Panier correctement supprimé."
#: purchase/views/reports.py:28
#: purchase/views/reports.py:29
msgid "No sale to report"
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"
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"
msgstr "Nombre de paniers par heure"
#: purchase/views/reports.py:118
#: purchase/views/reports.py:141
msgid "Turnover by hour"
msgstr "Chiffre d'affaires par heure"

View file

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

View file

@ -1,11 +1,18 @@
import datetime
from io import StringIO
from zoneinfo import ZoneInfo
import numpy as np
from django.conf import settings
from django.contrib import messages
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from matplotlib import pyplot as plt
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.views.utils import ProtectedViewsMixin
@ -31,7 +38,11 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
}
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)
context.update(
{
@ -41,6 +52,8 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
"average_basket_by_day": average_basket_by_day,
"products": products,
"products_plot": products_plot,
"products_sold_pie": products_sold_pie,
"products_turnover_pie": products_turnover_pie,
"by_hour_plot": by_hour_plot,
"payment_methods": PaymentMethod.objects.with_turnover().with_sold(),
"no_payment_method": Basket.objects.no_payment_method().priced(),
@ -48,26 +61,49 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
)
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)
fig, ax1 = plt.subplots()
x = np.arange(len(labels))
width = 0.4
fig: Figure = plt.figure()
fig.suptitle(_("Sales by product"))
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="y", labelcolor=color)
ax1.set_xticks(x, labels)
ax1.set_ylabel(_("# sold"), color=color)
color = "tab:blue"
ax2 = ax1.twinx()
ax2.bar(labels, turnover, width=0.4, color=color)
ax2: Axes = ax1.twinx()
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)
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()
image_data = StringIO()
fig.savefig(image_data, format="svg")
image_data.seek(0)
return image_data.getvalue()
img1 = self.get_image_from_fig(fig)
fig = plt.figure()
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):
labels = []
@ -80,25 +116,36 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
return labels, sold, turnover
def by_hour_plot(self, baskets):
counts, labels, turnovers = self.get_by_hour_data_for_plot(baskets)
fig, ax1 = plt.subplots()
labels, counts, turnovers = self.get_by_hour_data_for_plot(baskets)
hours_in_day = 24
fig: Figure = plt.figure()
fig.suptitle(_("Sales by hour"))
ax1: Axes = fig.add_subplot()
color = "tab:orange"
ax1.bar(labels, counts, width=1 / hours_in_day, color=color)
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_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"
ax2 = ax1.twinx()
ax2.bar(labels, turnovers, width=1 / (hours_in_day * 2), color=color)
ax2: Axes = ax1.twinx()
ax2.plot(labels, turnovers, ".-", 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()
image_data = StringIO()
fig.savefig(image_data, format="svg")
image_data.seek(0)
return image_data.getvalue()
return self.get_image_from_fig(fig)
def get_by_hour_data_for_plot(self, baskets):
current: datetime.datetime = baskets[0].created_at
@ -120,8 +167,15 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
if basket_index == len(baskets):
break
basket = baskets[basket_index]
labels.append(current)
labels.append(current.astimezone(ZoneInfo(settings.TIME_ZONE)))
counts.append(count)
turnovers.append(turnover)
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