mirror of
https://github.com/Crocmagnon/checkout.git
synced 2024-12-22 14:11:48 +01:00
Moar graphs
This commit is contained in:
parent
212b0d9713
commit
282318ac81
6 changed files with 138 additions and 58 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue