Total Profit

$197,768.32

Total Cost

$452,047.29

Avg. Sales Per Day

13

from django.contrib.humanize.templatetags.humanize import intcomma
from django.db.models import Avg, Count, F, Sum
from django.db.models.functions import ExtractMonth, ExtractYear
from django.utils.timezone import now

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from dashboards.component import Chart, Table
from dashboards.component.chart import ChartSerializer
from dashboards.component.layout import Card, ComponentLayout, Div
from dashboards.component.table import TableSerializer
from dashboards.component.text import Stat, StatData
from dashboards.dashboard import Dashboard
from dashboards.registry import registry
from dateutil.relativedelta import relativedelta
from plotly.subplots import make_subplots

from example.sales.models import Sale


def get_total_profit(**kwargs):
    profit = f'{Sale.objects.filter().aggregate(total_profit=Sum("profit_amount"))["total_profit"]:.2f}'
    return StatData(
        text="Total Profit",
        sub_text=f"${intcomma(profit)}",
    )


def get_total_cost(**kwargs):
    profit = f'{Sale.objects.filter().aggregate(total_cost=Sum("cost_amount"))["total_cost"]:.2f}'
    return StatData(
        text="Total Cost",
        sub_text=f"${intcomma(profit)}",
    )


def get_avg_sales_per_date(**kwargs):
    avg_sales_per_day = (
        Sale.objects.values("paid_date")
        .annotate(sales_per_day=Count("pk"))
        .aggregate(avg_sales_per_day=Avg("sales_per_day"))["avg_sales_per_day"]
    )
    return StatData(
        text="Avg. Sales Per Day",
        sub_text=f"{int(avg_sales_per_day)}",
    )


class ProfitByYearChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        return (
            Sale.objects.complete()
            .with_datetime()
            .values("channel", "year")
            .annotate(total_profit=Sum("profit_amount"))
            .values("year", "channel", "total_profit")
            .order_by("year")
        )

    def to_fig(self, df):
        df["year"] = df["year"].astype("str")
        fig = px.bar(
            df,
            y="year",
            x="total_profit",
            color="channel",
            text_auto=True,
            orientation="h",
        )
        fig.update_layout(xaxis_title="Year", yaxis_title="Profit")

        return fig

    class Meta:
        title = "PROFIT BY YEAR"


class SalesByChannelYearChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        return (
            Sale.objects.complete()
            .with_datetime()
            .values("year", "channel")
            .annotate(total_sales=Sum("paid_amount"))
            .values("year", "channel", "total_sales")
            .order_by("total_sales")
        )

    def to_fig(self, df):
        current_date = now()
        current_year = df[df["year"] == current_date.year]
        previous_year = df[df["year"] == current_date.year - 1]

        fig = make_subplots(
            rows=1,
            cols=2,
            shared_yaxes=True,
            subplot_titles=(
                f"Sales {current_date.year - 1}",
                f"Sales {current_date.year}",
            ),
        )

        fig.add_trace(
            go.Bar(
                x=current_year["channel"],
                y=current_year["total_sales"],
                name=current_date.year,
                text=current_year["total_sales"],
            ),
            row=1,
            col=1,
        )

        fig.add_trace(
            go.Bar(
                x=previous_year["channel"],
                y=previous_year["total_sales"],
                name=f"{current_date.year - 1}",
                text=current_year["total_sales"],
            ),
            row=1,
            col=2,
        )

        return fig

    class Meta:
        title = "SALES BY CHANNEL"


class QuantityByChannelYearChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        return (
            Sale.objects.complete()
            .with_datetime()
            .values("year", "channel")
            .annotate(qty=Count("id"))
            .values("year", "channel", "qty")
            .order_by("qty")
        )

    def to_fig(self, df):
        current_date = now()
        current_year = df[df["year"] == current_date.year]
        previous_year = df[df["year"] == current_date.year - 1]

        fig = make_subplots(
            rows=1,
            cols=2,
            shared_yaxes=True,
            specs=[[{"type": "pie"}, {"type": "pie"}]],
            subplot_titles=(
                f"QUANTITY {current_date.year - 1}",
                f"QUANTITY {current_date.year}",
            ),
        )

        fig.add_trace(
            go.Pie(
                labels=current_year["channel"],
                values=current_year["qty"],
                name=current_date.year,
                texttemplate="%{label}<br>%{percent}",
                textposition="inside",
                hole=0.4,
            ),
            row=1,
            col=1,
        )

        fig.add_trace(
            go.Pie(
                labels=previous_year["channel"],
                values=previous_year["qty"],
                name=f"{current_date.year - 1}",
                texttemplate="%{label}<br>%{percent}",
                textposition="inside",
                hole=0.4,
            ),
            row=1,
            col=2,
        )

        fig.update_layout(
            # Add annotations in the center of the donut pies.
            annotations=[
                dict(
                    text=f"{current_date.year - 1}",
                    x=0.22,
                    y=0.46,
                    font_size=20,
                    showarrow=False,
                ),
                dict(
                    text=f"{current_date.year}",
                    x=0.77,
                    y=0.46,
                    font_size=20,
                    showarrow=False,
                ),
            ]
        )

        return fig

    class Meta:
        title = "QUANTITY BY CHANNEL"


class QuantityByChannelChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        return (
            Sale.objects.complete()
            .with_datetime()
            .values("channel")
            .annotate(qty=Count("id"))
            .values("channel", "qty")
            .order_by("-qty")
        )

    def to_fig(self, df):
        fig = px.funnel_area(
            df,
            names="channel",
            values="qty",
        )
        return fig

    class Meta:
        title = "TOTAL SOLD BY CHANNEL"


class SalesByCountryYearChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        return (
            Sale.objects.complete()
            .with_datetime()
            .values("year", "customer__country", "channel")
            .annotate(total_profit=Sum("profit_amount"))
            .values("year", "customer__country", "channel", "total_profit")
        )

    def to_fig(self, df):
        fig = px.treemap(
            df,
            path=[px.Constant("all"), "year", "customer__country", "channel"],
            values="total_profit",
            color="customer__country",
            maxdepth=3,
        )
        fig.update_traces(root_color="lightgrey")

        return fig

    class Meta:
        title = "PROFIT BY COUNTRY PER YEAR"


class PaidVsCostMonthlyChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        from_datetime = now() - relativedelta(months=6)
        return (
            Sale.objects.complete()
            .with_datetime()
            .filter(paid_datetime__gte=from_datetime)
            .values("year", "month")
            .annotate(total_paid=Sum("paid_amount"), total_cost=Sum("cost_amount"))
            .values("year", "month", "total_paid", "total_cost")
        )

    def to_fig(self, df):
        df["date"] = pd.to_datetime(df[["year", "month"]].assign(DAY=1))

        fig = go.Figure()
        fig.add_trace(
            go.Bar(
                name="Total Paid",
                x=df["date"],
                y=df["total_paid"],
                text=df["total_paid"],
            ),
        )

        fig.add_trace(
            go.Bar(
                name="Total Cost",
                x=df["date"],
                y=df["total_cost"],
                text=df["total_cost"],
            ),
        )

        fig.update_layout(xaxis_title="Date", yaxis_title="Total")

        return fig

    class Meta:
        title = "COST VS PAID BY MONTH"


class SalesChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        current_date = now()
        return (
            Sale.objects.all()
            .annotate(
                paid_datetime=current_date - F("paid_date")
            )  # convert time delta for rolling demo.
            .values("paid_datetime")
            .filter()
            .annotate(
                month=ExtractMonth("paid_datetime"), year=ExtractYear("paid_datetime")
            )
            .values("month", "year")
            .annotate(total_profit=Sum("profit_amount"))
            .values("month", "year", "total_profit")
            .order_by("year", "month")
        )

    def to_fig(self, df):
        current_date = now()
        df["month"] = df["month"].astype(str)
        current_year = df[df["year"] == current_date.year]
        previous_year = df[df["year"] == current_date.year - 1]

        fig = go.Figure()
        fig.add_trace(
            go.Bar(
                x=current_year["month"],
                y=current_year["total_profit"],
                name=current_date.year,
            ),
        )

        fig.add_trace(
            go.Scatter(
                x=previous_year["month"],
                y=previous_year["total_profit"],
                name=f"Target ({current_date.year - 1})",
            ),
        )
        fig.update_layout(
            xaxis_title="Month No.", yaxis_title="Total Profit", hovermode="x unified"
        )

        return fig

    class Meta:
        title = "Current year sales"


class StatusPerMonthChartSerializer(ChartSerializer):
    def get_queryset(self, *args, **kwargs):
        return (
            Sale.objects.with_datetime()
            .values("status", "month", "year")
            .annotate(total=Count("status"))
            .values("status", "month", "year", "total")
            .order_by("-status", "year", "month")
        )

    def to_fig(self, df):
        df["date"] = pd.to_datetime(df[["year", "month"]].assign(DAY=1))

        fig = px.area(df, x="date", y="total", color="status")
        fig.update_layout(
            xaxis=dict(rangeslider=dict(visible=True), type="date"), hovermode="x"
        )

        return fig

    class Meta:
        title = "Completed & Refunded Per Month"


class CountryChannelBreakdownChartSerializer(ChartSerializer):
    def to_fig(self, df):
        fig = px.sunburst(
            df, path=["channel", "customer__country"], values="profit_amount"
        )

        return fig

    class Meta:
        title = "Country/Channel breakdown"
        fields = ["profit_amount", "channel", "customer__country"]
        model = Sale


class ChannelDistributionsChartSerializer(ChartSerializer):
    def to_fig(self, df):
        fig = px.density_heatmap(df, x="paid_amount", y="profit_amount")

        return fig

    class Meta:
        title = "Channel Distribution"
        fields = ["profit_amount", "paid_amount"]
        model = Sale


class AllSalesTableSerializer(TableSerializer):
    def get_queryset(self, *args, **kwargs):
        current_date = now()
        return (
            Sale.objects.filter(profit_amount__gt=33)
            .annotate(
                paid_datetime=current_date
                - F("paid_date")  # convert time delta for rolling demo.
            )
            .values("paid_datetime", "profit_amount", "channel", "customer")
        )

    class Meta:
        columns = {
            "paid_datetime": "Date",
            "customer__name": "Customer",
            "profit_amount": "Profit",
            "channel": "Channel",
        }
        order = ["-profit_amount"]


class SalesDashboard(Dashboard):
    total_profit = Stat(value=get_total_profit)
    total_cost = Stat(value=get_total_cost)
    avg_sales_per_date = Stat(value=get_avg_sales_per_date)
    daily_sales = Chart(defer=SalesChartSerializer)
    profit_by_year = Chart(defer=ProfitByYearChartSerializer)
    sales_by_channel = Chart(defer=SalesByChannelYearChartSerializer)
    sales_by_country = Chart(defer=SalesByCountryYearChartSerializer)
    quantity_by_channel_year = Chart(defer=QuantityByChannelYearChartSerializer)
    quantity_by_channel = Chart(defer=QuantityByChannelChartSerializer)
    paid_vs_cost = Chart(defer=PaidVsCostMonthlyChartSerializer)
    country_breakdown = Chart(defer=CountryChannelBreakdownChartSerializer)
    channel_distributions = Chart(defer=ChannelDistributionsChartSerializer)
    status_per_month = Chart(defer=StatusPerMonthChartSerializer)
    all_sales = Table(defer=AllSalesTableSerializer)

    class Meta:
        name = "Sales"

    class Layout(Dashboard.Layout):
        components = ComponentLayout(
            Card("total_profit", grid_css_classes="span-4"),
            Card("total_cost", grid_css_classes="span-4"),
            Card("avg_sales_per_date", grid_css_classes="span-4"),
            Card("sales_by_country", grid_css_classes="span-8"),
            Card("country_breakdown", grid_css_classes="span-4"),
            Card("sales_by_channel", grid_css_classes="span-6"),
            Card("quantity_by_channel_year", grid_css_classes="span-6"),
            Card("quantity_by_channel", grid_css_classes="span-4"),
            Card("paid_vs_cost", grid_css_classes="span-8"),
            Card("profit_by_year", grid_css_classes="span-6"),
            Card("channel_distributions", grid_css_classes="span-6"),
            Card("daily_sales", grid_css_classes="span-6"),
            Card("status_per_month", grid_css_classes="span-6"),
        )


registry.register(SalesDashboard)