diff --git a/src/common/locale/en/LC_MESSAGES/django.po b/src/common/locale/en/LC_MESSAGES/django.po index 89838bc..c3ad315 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 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 \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 4e503fb..995f0b8 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 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 \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 6bea479..bf8a63b 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 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 \n" "Language-Team: LANGUAGE \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 "" diff --git a/src/purchase/locale/fr/LC_MESSAGES/django.po b/src/purchase/locale/fr/LC_MESSAGES/django.po index 8c04375..21c611c 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 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 \n" "Language-Team: LANGUAGE \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" diff --git a/src/purchase/templates/purchase/reports.html b/src/purchase/templates/purchase/reports.html index 9b644db..00ed1fd 100644 --- a/src/purchase/templates/purchase/reports.html +++ b/src/purchase/templates/purchase/reports.html @@ -34,6 +34,8 @@

{% translate "Products" %}

{% include "purchase/snippets/report_products.html" %} {{ products_plot|safe }} + {{ products_sold_pie|safe }} + {{ products_turnover_pie|safe }}

{% translate "Turnover by payment method" %}

{% include "purchase/snippets/report_payment_methods.html" %} diff --git a/src/purchase/views/reports.py b/src/purchase/views/reports.py index ab15eb3..15980db 100644 --- a/src/purchase/views/reports.py +++ b/src/purchase/views/reports.py @@ -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