mirror of
https://github.com/Crocmagnon/checkout.git
synced 2024-11-22 16:18:03 +01:00
Replace reports CBV with FBV
This commit is contained in:
parent
60660c0abf
commit
91d64c6ce4
2 changed files with 143 additions and 145 deletions
|
@ -6,7 +6,7 @@ from purchase.views import (
|
||||||
NewBasketView,
|
NewBasketView,
|
||||||
UpdateBasketView,
|
UpdateBasketView,
|
||||||
)
|
)
|
||||||
from purchase.views.reports import ReportsView
|
from purchase.views.reports import reports
|
||||||
|
|
||||||
app_name = "purchase"
|
app_name = "purchase"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -14,5 +14,5 @@ urlpatterns = [
|
||||||
path("new/", NewBasketView.as_view(), name="new"),
|
path("new/", NewBasketView.as_view(), name="new"),
|
||||||
path("<int:pk>/update/", UpdateBasketView.as_view(), name="update"),
|
path("<int:pk>/update/", UpdateBasketView.as_view(), name="update"),
|
||||||
path("<int:pk>/delete/", DeleteBasketView.as_view(), name="delete"),
|
path("<int:pk>/delete/", DeleteBasketView.as_view(), name="delete"),
|
||||||
path("reports/", ReportsView.as_view(), name="reports"),
|
path("reports/", reports, name="reports"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,8 +5,9 @@ from zoneinfo import ZoneInfo
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import permission_required
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import TemplateView
|
|
||||||
from matplotlib import pyplot as plt
|
from matplotlib import pyplot as plt
|
||||||
from matplotlib import ticker
|
from matplotlib import ticker
|
||||||
from matplotlib.axes import Axes
|
from matplotlib.axes import Axes
|
||||||
|
@ -15,167 +16,164 @@ from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, HourLocator
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
from purchase.models import Basket, PaymentMethod, Product, ProductQuerySet
|
from purchase.models import Basket, PaymentMethod, Product, ProductQuerySet
|
||||||
from purchase.views.utils import ProtectedViewsMixin
|
|
||||||
|
|
||||||
|
|
||||||
class ReportsView(ProtectedViewsMixin, TemplateView):
|
@permission_required("purchase.view_basket")
|
||||||
permission_required = ["purchase.view_basket"]
|
def reports(request):
|
||||||
template_name = "purchase/reports.html"
|
template_name = "purchase/reports.html"
|
||||||
|
baskets = list(Basket.objects.priced().order_by("created_at"))
|
||||||
|
if not baskets:
|
||||||
|
messages.warning(request, _("No sale to report"))
|
||||||
|
return TemplateResponse(request, template_name, {})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
dates = Basket.objects.values_list("created_at__date", flat=True).distinct()
|
||||||
context = super().get_context_data(**kwargs)
|
average_basket_by_day = {
|
||||||
baskets = list(Basket.objects.priced().order_by("created_at"))
|
date: Basket.objects.by_date(date).average_basket() for date in dates
|
||||||
if not baskets:
|
}
|
||||||
messages.warning(self.request, _("No sale to report"))
|
turnover_by_day = {date: Basket.objects.by_date(date).turnover() for date in dates}
|
||||||
return context
|
|
||||||
|
|
||||||
dates = Basket.objects.values_list("created_at__date", flat=True).distinct()
|
products = Product.objects.with_turnover().with_sold()
|
||||||
average_basket_by_day = {
|
(
|
||||||
date: Basket.objects.by_date(date).average_basket() for date in dates
|
products_plot,
|
||||||
}
|
products_sold_pie,
|
||||||
turnover_by_day = {
|
products_turnover_pie,
|
||||||
date: Basket.objects.by_date(date).turnover() for date in dates
|
) = get_products_plots(products)
|
||||||
}
|
|
||||||
|
|
||||||
products = Product.objects.with_turnover().with_sold()
|
context = {
|
||||||
(
|
"turnover": Basket.objects.turnover(),
|
||||||
products_plot,
|
"turnover_by_day": turnover_by_day,
|
||||||
products_sold_pie,
|
"average_basket": Basket.objects.average_basket(),
|
||||||
products_turnover_pie,
|
"average_basket_by_day": average_basket_by_day,
|
||||||
) = self.get_products_plots(products)
|
"products": products,
|
||||||
by_hour_plot = self.by_hour_plot(baskets)
|
"products_plot": products_plot,
|
||||||
context.update(
|
"products_sold_pie": products_sold_pie,
|
||||||
{
|
"products_turnover_pie": products_turnover_pie,
|
||||||
"turnover": Basket.objects.turnover(),
|
"by_hour_plot": by_hour_plot(baskets),
|
||||||
"turnover_by_day": turnover_by_day,
|
"payment_methods": PaymentMethod.objects.with_turnover().with_sold(),
|
||||||
"average_basket": Basket.objects.average_basket(),
|
"no_payment_method": Basket.objects.no_payment_method().priced(),
|
||||||
"average_basket_by_day": average_basket_by_day,
|
}
|
||||||
"products": products,
|
return TemplateResponse(request, template_name, context)
|
||||||
"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))
|
def get_products_plots(products: ProductQuerySet):
|
||||||
width = 0.4
|
labels, sold, turnover = get_products_data_for_plot(products)
|
||||||
fig: Figure = plt.figure()
|
|
||||||
fig.suptitle(_("Sales by product"))
|
|
||||||
|
|
||||||
color = "tab:orange"
|
x = np.arange(len(labels))
|
||||||
ax1: Axes = fig.add_subplot()
|
width = 0.4
|
||||||
bar: BarContainer = ax1.bar(x - width / 2, sold, width=width, color=color)
|
fig: Figure = plt.figure()
|
||||||
ax1.bar_label(bar)
|
fig.suptitle(_("Sales by product"))
|
||||||
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"
|
color = "tab:orange"
|
||||||
ax2: Axes = ax1.twinx()
|
ax1: Axes = fig.add_subplot()
|
||||||
bar = ax2.bar(x + width / 2, turnover, width=width, color=color)
|
bar: BarContainer = ax1.bar(x - width / 2, sold, width=width, color=color)
|
||||||
ax2.bar_label(bar, fmt="%d€")
|
ax1.bar_label(bar)
|
||||||
ax2.set_ylabel(_("Turnover by product"), color=color)
|
ax1.tick_params(axis="x", rotation=15)
|
||||||
ax2.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f€"))
|
ax1.tick_params(axis="y", labelcolor=color)
|
||||||
ax2.tick_params(axis="y", labelcolor=color)
|
ax1.set_xticks(x, labels)
|
||||||
|
ax1.set_ylabel(_("# sold"), color=color)
|
||||||
|
|
||||||
fig.tight_layout()
|
color = "tab:blue"
|
||||||
img1 = self.get_image_from_fig(fig)
|
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 = plt.figure()
|
fig.tight_layout()
|
||||||
fig.suptitle(_("# sold"))
|
img1 = get_image_from_fig(fig)
|
||||||
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 = plt.figure()
|
||||||
fig.suptitle(_("Turnover by product"))
|
fig.suptitle(_("# sold"))
|
||||||
ax1 = fig.add_subplot()
|
ax1 = fig.add_subplot()
|
||||||
ax1.pie(turnover, labels=labels, autopct="%d%%")
|
ax1.pie(sold, labels=labels, autopct="%d%%")
|
||||||
fig.tight_layout()
|
fig.tight_layout()
|
||||||
img3 = self.get_image_from_fig(fig)
|
img2 = get_image_from_fig(fig)
|
||||||
|
|
||||||
return img1, img2, img3
|
fig = plt.figure()
|
||||||
|
fig.suptitle(_("Turnover by product"))
|
||||||
|
ax1 = fig.add_subplot()
|
||||||
|
ax1.pie(turnover, labels=labels, autopct="%d%%")
|
||||||
|
fig.tight_layout()
|
||||||
|
img3 = get_image_from_fig(fig)
|
||||||
|
|
||||||
def get_products_data_for_plot(self, products):
|
return img1, img2, img3
|
||||||
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"
|
def get_products_data_for_plot(products):
|
||||||
ax1.bar(labels, counts, width=1 / hours_in_day, color=color)
|
labels = []
|
||||||
ax1.tick_params(axis="x", rotation=15)
|
sold = []
|
||||||
ax1.xaxis.set_minor_locator(HourLocator())
|
turnover = []
|
||||||
tz = ZoneInfo(settings.TIME_ZONE)
|
for product in products:
|
||||||
locator = AutoDateLocator(tz=tz)
|
labels.append(product.name)
|
||||||
ax1.xaxis.set_major_locator(locator)
|
sold.append(product.sold)
|
||||||
ax1.xaxis.set_major_formatter(ConciseDateFormatter(locator, tz=tz))
|
turnover.append(product.turnover / 100)
|
||||||
ax1.set_ylabel(_("Basket count by hour"), color=color)
|
return labels, sold, turnover
|
||||||
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()
|
def by_hour_plot(baskets):
|
||||||
return self.get_image_from_fig(fig)
|
labels, counts, turnovers = 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()
|
||||||
|
|
||||||
def get_by_hour_data_for_plot(self, baskets):
|
color = "tab:orange"
|
||||||
current: datetime.datetime = baskets[0].created_at
|
ax1.bar(labels, counts, width=1 / hours_in_day, color=color)
|
||||||
current = current.replace(minute=0, second=0, microsecond=0)
|
ax1.tick_params(axis="x", rotation=15)
|
||||||
end: datetime.datetime = baskets[-1].created_at
|
ax1.xaxis.set_minor_locator(HourLocator())
|
||||||
basket_index = 0
|
tz = ZoneInfo(settings.TIME_ZONE)
|
||||||
labels = []
|
locator = AutoDateLocator(tz=tz)
|
||||||
counts = []
|
ax1.xaxis.set_major_locator(locator)
|
||||||
turnovers = []
|
ax1.xaxis.set_major_formatter(ConciseDateFormatter(locator, tz=tz))
|
||||||
while current < end:
|
ax1.set_ylabel(_("Basket count by hour"), color=color)
|
||||||
end_slot = current + datetime.timedelta(hours=1)
|
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 get_image_from_fig(fig)
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_hour_data_for_plot(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]
|
basket = baskets[basket_index]
|
||||||
count = 0
|
labels.append(current.astimezone(ZoneInfo(settings.TIME_ZONE)))
|
||||||
turnover = 0
|
counts.append(count)
|
||||||
while basket.created_at < end_slot:
|
turnovers.append(turnover)
|
||||||
count += 1
|
current = end_slot
|
||||||
turnover += basket.price / 100
|
return labels, counts, turnovers
|
||||||
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()
|
def get_image_from_fig(fig):
|
||||||
fig.savefig(image_data, format="svg")
|
image_data = StringIO()
|
||||||
image_data.seek(0)
|
fig.savefig(image_data, format="svg")
|
||||||
img1 = image_data.getvalue()
|
image_data.seek(0)
|
||||||
return img1
|
img1 = image_data.getvalue()
|
||||||
|
return img1
|
||||||
|
|
Loading…
Reference in a new issue