Source code for shuup.admin.modules.sales_dashboard.dashboard

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.

from __future__ import unicode_literals

import six
from babel.dates import format_date
from collections import OrderedDict
from datetime import date, time, timedelta
from dateutil import rrule
from decimal import Decimal
from django.db.models import Avg, Count, Sum
from django.utils.translation import ugettext_lazy as _

from shuup.admin.dashboard import (
    ChartDataType,
    ChartType,
    DashboardChartBlock,
    DashboardContentBlock,
    DashboardMoneyBlock,
    MixedChart,
)
from shuup.core.models import Order, Shop
from shuup.core.pricing import TaxfulPrice
from shuup.core.utils.query import group_by_period
from shuup.utils.dates import get_year_and_month_format, local_now, to_aware
from shuup.utils.i18n import get_current_babel_locale


[docs]def get_orders_for_shop(request, currency=None): shop = request.shop if not currency: currency = shop.currency return Order.objects.filter(shop=shop, currency=currency)
[docs]def get_orders_by_currency(currency): return Order.objects.filter(currency=currency)
[docs]def month_iter(start_date, end_date): return ((d.month, d.year) for d in rrule.rrule(rrule.MONTHLY, dtstart=start_date, until=end_date))
[docs]class OrderValueChartDashboardBlock(DashboardChartBlock): default_size = "small" def __init__(self, id, request, **kwargs): shop = request.shop self.cached_chart = None self.request = request self.currency = shop.currency super(OrderValueChartDashboardBlock, self).__init__(id, **kwargs) self.sort_order = 2 @property def size(self): data_size = 0 for dataset in self.get_chart().datasets: data_size = max(data_size, len(dataset["data"])) # the size will be dynamic. small for periods up to 4 months, otherwise medium return "medium" if data_size > 4 else "small" @size.setter def size(self, value): # do not raise! pass
[docs] def get_chart(self): if self.cached_chart is not None: return self.cached_chart chart_options = {"scales": {"yAxes": [{"ticks": {"beginAtZero": True}}]}} today = date.today() chart_start_date = today - timedelta(days=365) orders = get_orders_for_shop(self.request) sum_sales_data = group_by_period( orders.valid().since((today - chart_start_date).days), "order_date", "month", sum=Sum("taxful_total_price_value"), ) for (month, year) in month_iter(chart_start_date, today): sales_date = date(year, month, 1) if sales_date not in sum_sales_data: sum_sales_data[sales_date] = {"sum": Decimal(0)} # sort and recreated the ordered dict since we've put new items into sum_sales_data = OrderedDict(sorted(six.iteritems(sum_sales_data), key=lambda x: x[0])) locale = get_current_babel_locale() labels = [format_date(k, format=get_year_and_month_format(locale), locale=locale) for k in sum_sales_data] mixed_chart = MixedChart( title=_("Sales per Month (past 12 months)"), labels=labels, data_type=ChartDataType.CURRENCY, options=chart_options, currency=self.currency, locale=locale, ) cumulative_sales = [] average_sales = [] # only calculate cumulative and average if there are at least 3 months if len(sum_sales_data) >= 3: count = 0 total = Decimal() for month_sale in sum_sales_data.values(): total = total + month_sale["sum"] cumulative_sales.append(total) average_sales.append(total / (count + 1)) count = count + 1 # this will be on top of all bars if average_sales: mixed_chart.add_data(_("Average Sales"), [v for v in average_sales], ChartType.LINE) # this will be under the cummulative bars mixed_chart.add_data(_("Sales"), [v["sum"] for v in sum_sales_data.values()], ChartType.BAR) # this will be under all others charts if cumulative_sales: mixed_chart.add_data(_("Cumulative Total Sales"), [v for v in cumulative_sales], ChartType.BAR) self.cached_chart = mixed_chart return mixed_chart
[docs]def get_subtitle(count): return _("Based on %d orders") % count
[docs]def get_sales_of_the_day_block(request, currency=None): orders = get_orders_for_shop(request) if not currency: shop = request.shop currency = shop.currency # Sales of the day todays_order_data = orders.complete().since(0).aggregate(count=Count("id"), sum=Sum("taxful_total_price_value")) return DashboardMoneyBlock( id="todays_order_sum", color="green", title=_("Today's Sales"), value=(todays_order_data.get("sum") or 0), currency=currency, icon="fa fa-calculator", subtitle=get_subtitle(todays_order_data.get("count")), )
[docs]def get_lifetime_sales_block(request, currency=None): orders = get_orders_for_shop(request) if not currency: shop = request.shop currency = shop.currency # Lifetime sales lifetime_sales_data = orders.complete().aggregate(count=Count("id"), sum=Sum("taxful_total_price_value")) return DashboardMoneyBlock( id="lifetime_sales_sum", color="green", title=_("Lifetime Sales"), value=(lifetime_sales_data.get("sum") or 0), currency=currency, icon="fa fa-line-chart", subtitle=get_subtitle(lifetime_sales_data.get("count")), sort_order=1, )
[docs]def get_avg_purchase_size_block(request, currency=None): orders = get_orders_for_shop(request) shop = request.shop if not currency: currency = shop.currency lifetime_sales_data = orders.complete().aggregate(count=Count("id"), sum=Sum("taxful_total_price_value")) # Average size of purchase with amount of orders it is calculated from average_purchase_size = Order.objects.filter(shop=shop).aggregate( count=Count("id"), sum=Avg("taxful_total_price_value") ) return DashboardMoneyBlock( id="average_purchase_sum", color="blue", title=_("Average Purchase"), value=(average_purchase_size.get("sum") or 0), currency=currency, icon="fa fa-shopping-cart", subtitle=get_subtitle(lifetime_sales_data.get("count")), sort_order=1, )
[docs]def get_open_orders_block(request, currency=None): orders = get_orders_for_shop(request) if not currency: shop = request.shop currency = shop.currency # Open orders / open orders value open_order_data = orders.incomplete().aggregate(count=Count("id"), sum=Sum("taxful_total_price_value")) return DashboardMoneyBlock( id="open_orders_sum", color="orange", title=_("Open Orders Value"), value=TaxfulPrice((open_order_data.get("sum") or 0), currency), currency=currency, icon="fa fa-inbox", subtitle=get_subtitle(open_order_data.get("count")), sort_order=1, )
[docs]def get_order_value_chart_dashboard_block(request, currency): return OrderValueChartDashboardBlock(id="order_value_chart", request=request)
[docs]def get_order_overview_for_date_range(currency, start_date, end_date, shop=None): if not shop: shop = Shop.objects.first() orders = get_orders_by_currency(currency).filter(shop=shop).complete() orders_in_range = orders.in_date_range(start_date, end_date) q = orders_in_range.aggregate( num_orders=Count("id"), num_customers=Count("customer", distinct=True), sales=Sum("taxful_total_price_value") ) anon_orders = orders_in_range.filter(customer__isnull=True).aggregate(num_orders=Count("id")) q["num_customers"] += anon_orders["num_orders"] q["sales"] = TaxfulPrice(q["sales"] or 0, currency) return q
[docs]def get_shop_overview_block(request, currency, for_date=None): end = to_aware(for_date, time=time.max) if for_date else local_now() start_of_day = to_aware(end.date(), time=time.min) start_of_month = start_of_day.replace(day=1) start_of_year = start_of_day.replace(month=1, day=1) shop = request.shop if not currency: currency = shop.currency daily = get_order_overview_for_date_range(currency, start_of_day, end, shop=shop) mtd = get_order_overview_for_date_range(currency, start_of_month, end, shop=shop) ytd = get_order_overview_for_date_range(currency, start_of_year, end, shop=shop) totals = ( get_orders_by_currency(currency) .complete() .filter(shop=shop) .aggregate( num_orders=Count("id"), num_customers=Count("customer", distinct=True), sales=Sum("taxful_total_price_value"), ) ) anon_orders = ( get_orders_by_currency(currency) .complete() .filter(customer__isnull=True, shop=shop) .aggregate(num_orders=Count("id")) ) totals["num_customers"] += anon_orders["num_orders"] totals["sales"] = TaxfulPrice(totals["sales"] or 0, currency) block = DashboardContentBlock.by_rendering_template( "store_overview", request, "shuup/admin/sales_dashboard/_store_overview_dashboard_block.jinja", {"daily": daily, "mtd": mtd, "ytd": ytd, "totals": totals}, ) block.size = "medium" block.sort_order = 0 return block
[docs]def get_recent_orders_block(request, currency=None): orders = get_orders_for_shop(request).valid().order_by("-order_date")[:5] block = DashboardContentBlock.by_rendering_template( "recent_orders", request, "shuup/admin/sales_dashboard/_recent_orders_dashboard_block.jinja", {"orders": orders} ) block.size = "medium" block.sort_order = 2 return block