checkout/src/purchase/views/reports.py
2022-04-27 23:00:38 +02:00

182 lines
6.7 KiB
Python

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
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)
baskets = list(Basket.objects.priced().order_by("created_at"))
if not baskets:
messages.warning(self.request, _("No sale to report"))
return context
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
}
products = Product.objects.with_turnover().with_sold()
(
products_plot,
products_sold_pie,
products_turnover_pie,
) = self.get_products_plots(products)
by_hour_plot = self.by_hour_plot(baskets)
context.update(
{
"turnover": Basket.objects.turnover(),
"turnover_by_day": turnover_by_day,
"average_basket": Basket.objects.average_basket(),
"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(),
}
)
return context
def get_products_plots(self, products: ProductQuerySet):
labels, sold, turnover = self.get_products_data_for_plot(products)
x = np.arange(len(labels))
width = 0.4
fig: Figure = plt.figure()
fig.suptitle(_("Sales by product"))
color = "tab:orange"
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: 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)
ax2.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f"))
ax2.tick_params(axis="y", labelcolor=color)
fig.tight_layout()
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 = []
sold = []
turnover = []
for product in products:
labels.append(product.name)
sold.append(product.sold)
turnover.append(product.turnover / 100)
return labels, sold, turnover
def by_hour_plot(self, baskets):
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: Axes = ax1.twinx()
ax2.plot(labels, turnovers, ".-", color=color)
ax2.set_ylabel(_("Turnover by hour"), color=color)
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()
return self.get_image_from_fig(fig)
def get_by_hour_data_for_plot(self, baskets):
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
while basket.created_at < end_slot:
count += 1
turnover += basket.price / 100
basket_index += 1
if basket_index == len(baskets):
break
basket = baskets[basket_index]
labels.append(current.astimezone(ZoneInfo(settings.TIME_ZONE)))
counts.append(count)
turnovers.append(turnover)
current = end_slot
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