checkout/src/purchase/views/reports.py

182 lines
6.7 KiB
Python
Raw Normal View History

2022-04-26 20:08:13 +02:00
import datetime
2022-04-26 19:25:09 +02:00
from io import StringIO
2022-04-27 22:59:52 +02:00
from zoneinfo import ZoneInfo
2022-04-26 19:25:09 +02:00
2022-04-27 22:59:52 +02:00
import numpy as np
from django.conf import settings
2022-04-26 21:10:03 +02:00
from django.contrib import messages
2022-04-26 19:25:09 +02:00
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
2022-04-26 19:25:09 +02:00
from matplotlib import pyplot as plt
from matplotlib import ticker
2022-04-27 22:59:52 +02:00
from matplotlib.axes import Axes
from matplotlib.container import BarContainer
from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, HourLocator
from matplotlib.figure import Figure
2022-04-27 20:46:50 +02:00
from purchase.models import Basket, PaymentMethod, Product, ProductQuerySet
2022-04-25 18:59:32 +02:00
from purchase.views.utils import ProtectedViewsMixin
2022-04-25 18:59:32 +02:00
class ReportsView(ProtectedViewsMixin, TemplateView):
permission_required = ["purchase.view_basket"]
template_name = "purchase/reports.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2022-04-26 21:10:03 +02:00
baskets = list(Basket.objects.priced().order_by("created_at"))
if not baskets:
messages.warning(self.request, _("No sale to report"))
return context
2022-04-26 18:19:53 +02:00
dates = Basket.objects.values_list("created_at__date", flat=True).distinct()
average_basket_by_day = {
date: Basket.objects.by_date(date).average_basket() for date in dates
}
turnover_by_day = {
date: Basket.objects.by_date(date).turnover() for date in dates
}
2022-04-26 19:25:09 +02:00
products = Product.objects.with_turnover().with_sold()
2022-04-27 22:59:52 +02:00
(
products_plot,
products_sold_pie,
products_turnover_pie,
) = self.get_products_plots(products)
2022-04-26 20:08:13 +02:00
by_hour_plot = self.by_hour_plot(baskets)
context.update(
{
2022-04-26 18:19:53 +02:00
"turnover": Basket.objects.turnover(),
"turnover_by_day": turnover_by_day,
"average_basket": Basket.objects.average_basket(),
"average_basket_by_day": average_basket_by_day,
2022-04-26 19:25:09 +02:00
"products": products,
2022-04-26 20:11:36 +02:00
"products_plot": products_plot,
2022-04-27 22:59:52 +02:00
"products_sold_pie": products_sold_pie,
"products_turnover_pie": products_turnover_pie,
2022-04-26 20:08:13 +02:00
"by_hour_plot": by_hour_plot,
2022-04-25 18:59:32 +02:00
"payment_methods": PaymentMethod.objects.with_turnover().with_sold(),
"no_payment_method": Basket.objects.no_payment_method().priced(),
}
)
return context
2022-04-26 19:25:09 +02:00
2022-04-27 22:59:52 +02:00
def get_products_plots(self, products: ProductQuerySet):
labels, sold, turnover = self.get_products_data_for_plot(products)
2022-04-27 22:59:52 +02:00
x = np.arange(len(labels))
width = 0.4
fig: Figure = plt.figure()
fig.suptitle(_("Sales by product"))
color = "tab:orange"
2022-04-27 22:59:52 +02:00
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)
2022-04-27 22:59:52 +02:00
ax1.tick_params(axis="y", labelcolor=color)
ax1.set_xticks(x, labels)
ax1.set_ylabel(_("# sold"), color=color)
color = "tab:blue"
2022-04-27 22:59:52 +02:00
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)
2022-04-27 22:59:52 +02:00
ax2.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f"))
ax2.tick_params(axis="y", labelcolor=color)
fig.tight_layout()
2022-04-27 22:59:52 +02:00
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):
2022-04-26 19:25:09 +02:00
labels = []
2022-04-26 20:11:36 +02:00
sold = []
turnover = []
2022-04-26 19:25:09 +02:00
for product in products:
labels.append(product.name)
2022-04-26 20:11:36 +02:00
sold.append(product.sold)
turnover.append(product.turnover / 100)
return labels, sold, turnover
2022-04-26 19:25:09 +02:00
def by_hour_plot(self, baskets):
2022-04-27 22:59:52 +02:00
labels, counts, turnovers = self.get_by_hour_data_for_plot(baskets)
hours_in_day = 24
2022-04-27 22:59:52 +02:00
fig: Figure = plt.figure()
fig.suptitle(_("Sales by hour"))
ax1: Axes = fig.add_subplot()
2022-04-26 20:11:36 +02:00
color = "tab:orange"
ax1.bar(labels, counts, width=1 / hours_in_day, color=color)
2022-04-26 20:11:36 +02:00
ax1.tick_params(axis="x", rotation=15)
2022-04-27 22:59:52 +02:00
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)
2022-04-27 22:59:52 +02:00
ax1.set_yticks(np.linspace(*ax1.get_ybound(), 8))
ax1.tick_params(axis="y", labelcolor=color)
ax1.grid(visible=True, which="major", axis="y")
2022-04-26 20:11:36 +02:00
color = "tab:blue"
2022-04-27 22:59:52 +02:00
ax2: Axes = ax1.twinx()
ax2.plot(labels, turnovers, ".-", color=color)
ax2.set_ylabel(_("Turnover by hour"), color=color)
2022-04-27 22:59:52 +02:00
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"))
2022-04-26 20:11:36 +02:00
fig.tight_layout()
2022-04-27 22:59:52 +02:00
return self.get_image_from_fig(fig)
2022-04-26 20:08:13 +02:00
def get_by_hour_data_for_plot(self, baskets):
2022-04-26 20:08:13 +02:00
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
2022-04-26 20:46:44 +02:00
while basket.created_at < end_slot:
2022-04-26 20:08:13 +02:00
count += 1
turnover += basket.price / 100
basket_index += 1
2022-04-26 20:46:44 +02:00
if basket_index == len(baskets):
break
2022-04-26 20:08:13 +02:00
basket = baskets[basket_index]
2022-04-27 22:59:52 +02:00
labels.append(current.astimezone(ZoneInfo(settings.TIME_ZONE)))
2022-04-26 20:08:13 +02:00
counts.append(count)
turnovers.append(turnover)
current = end_slot
2022-04-27 22:59:52 +02:00
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