From 71389ee27838addb3c750e45d439f59bd49eddee Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Mon, 17 Nov 2025 22:42:38 +0300 Subject: [PATCH] Features: 1) Add customer mix and shipped vs digital metrics to dashboard context; Fixes: 1) Replace failed daily stats with empty lists instead of crashing; 2) Fix inconsistent variable scoping and cleanup; Extra: 1) Refactor daily stats logic to handle exceptions gracefully; 2) Improve readability by grouping related metrics; 3) Add type annotations for new context variables. --- .../admin/dashboard/_customers_mix.html | 28 ++ .../admin/dashboard/_daily_sales.html | 129 ++++++ .../admin/dashboard/_income_overview.html | 118 ++++++ .../core/templates/admin/dashboard/_kpis.html | 39 ++ .../admin/dashboard/_most_returned.html | 27 ++ .../admin/dashboard/_product_lists.html | 71 ++++ .../admin/dashboard/_quick_links.html | 15 + .../admin/dashboard/_shipped_vs_digital.html | 29 ++ .../admin/dashboard/_top_categories.html | 26 ++ engine/core/templates/admin/index.html | 383 +----------------- engine/core/utils/commerce.py | 157 ++++++- engine/core/views.py | 80 +++- evibes/settings/jazzmin.py | 11 - 13 files changed, 717 insertions(+), 396 deletions(-) create mode 100644 engine/core/templates/admin/dashboard/_customers_mix.html create mode 100644 engine/core/templates/admin/dashboard/_daily_sales.html create mode 100644 engine/core/templates/admin/dashboard/_income_overview.html create mode 100644 engine/core/templates/admin/dashboard/_kpis.html create mode 100644 engine/core/templates/admin/dashboard/_most_returned.html create mode 100644 engine/core/templates/admin/dashboard/_product_lists.html create mode 100644 engine/core/templates/admin/dashboard/_quick_links.html create mode 100644 engine/core/templates/admin/dashboard/_shipped_vs_digital.html create mode 100644 engine/core/templates/admin/dashboard/_top_categories.html delete mode 100644 evibes/settings/jazzmin.py diff --git a/engine/core/templates/admin/dashboard/_customers_mix.html b/engine/core/templates/admin/dashboard/_customers_mix.html new file mode 100644 index 00000000..74a5af15 --- /dev/null +++ b/engine/core/templates/admin/dashboard/_customers_mix.html @@ -0,0 +1,28 @@ +{% load i18n unfold %} + +{% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Customers mix (30d)" %} + {% endcomponent %} + {% if customers_mix.total|default:0 > 0 %} +
+
+ {% trans "New customers" %} + {{ customers_mix.new }} ({{ customers_mix.new_pct }}%) +
+ {% component "unfold/components/progress.html" with value=customers_mix.new_pct title='' description='' %} + {% endcomponent %} + +
+ {% trans "Returning customers" %} + {{ customers_mix.returning }} ({{ customers_mix.returning_pct }}%) +
+ {% component "unfold/components/progress.html" with value=customers_mix.returning_pct title='' description='' %} + {% endcomponent %} +
+ {% else %} + {% component "unfold/components/text.html" %} + {% trans "No customer activity in the last 30 days." %} + {% endcomponent %} + {% endif %} +{% endcomponent %} diff --git a/engine/core/templates/admin/dashboard/_daily_sales.html b/engine/core/templates/admin/dashboard/_daily_sales.html new file mode 100644 index 00000000..58c30b0f --- /dev/null +++ b/engine/core/templates/admin/dashboard/_daily_sales.html @@ -0,0 +1,129 @@ +{% load i18n unfold static %} + +{% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Daily sales (30d)" %} + {% endcomponent %} +
+ +
+ + {{ daily_labels|json_script:"daily-labels" }} + {{ daily_orders|json_script:"daily-orders" }} + {{ daily_gross|json_script:"daily-gross" }} + +{% endcomponent %} diff --git a/engine/core/templates/admin/dashboard/_income_overview.html b/engine/core/templates/admin/dashboard/_income_overview.html new file mode 100644 index 00000000..c42c8c29 --- /dev/null +++ b/engine/core/templates/admin/dashboard/_income_overview.html @@ -0,0 +1,118 @@ +{% load i18n unfold arith %} + +{% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 %} + {% with total=gross|add:returns %} + {% component "unfold/components/card.html" with class="xl:col-span-2" %} + {% component "unfold/components/title.html" %} + {% trans "Income overview" %} + {% endcomponent %} + + {% with net=revenue_net_30|default:0 %} + {% with tax_amt=gross|sub:net %} + {% with returns_capped=returns %} + {% if returns > gross %} + {% with returns_capped=gross %}{% endwith %} + {% endif %} + {% with tax_amt_pos=tax_amt %} + {% if tax_amt_pos < 0 %} + {% with tax_amt_pos=0 %}{% endwith %} + {% endif %} + {% with net_for_pie=gross|sub:tax_amt_pos|sub:returns_capped %} + {% if net_for_pie < 0 %} + {% with net_for_pie=0 %}{% endwith %} + {% endif %} +
+
+ +
+
+
+ + {% trans "Net" %}: + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ net }} +
+ {% if tax_amt_pos > 0 %} +
+ + {% trans "Taxes" %}: + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ tax_amt_pos|floatformat:2 }} +
+ {% endif %} +
+ + {% trans "Returns" %}: + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns }} +
+
+ + {% trans "Gross (pie total)" %}: + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ gross }} +
+
+
+ + + {% endwith %} + {% endwith %} + {% endwith %} + {% endwith %} + {% endwith %} + {% endcomponent %} + {% endwith %} +{% endwith %} diff --git a/engine/core/templates/admin/dashboard/_kpis.html b/engine/core/templates/admin/dashboard/_kpis.html new file mode 100644 index 00000000..85483682 --- /dev/null +++ b/engine/core/templates/admin/dashboard/_kpis.html @@ -0,0 +1,39 @@ +{% load i18n unfold %} + +
+ {% component "unfold/components/card.html" %} + {% component "unfold/components/text.html" %} + {% trans "Revenue (gross, 30d)" %} + {% endcomponent %} + {% component "unfold/components/title.html" %} + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_gross_30|default:0 }} + {% endcomponent %} + {% endcomponent %} + + {% component "unfold/components/card.html" %} + {% component "unfold/components/text.html" %} + {% trans "Revenue (net, 30d)" %} + {% endcomponent %} + {% component "unfold/components/title.html" %} + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_net_30|default:0 }} + {% endcomponent %} + {% endcomponent %} + + {% component "unfold/components/card.html" %} + {% component "unfold/components/text.html" %} + {% trans "Returns (30d)" %} + {% endcomponent %} + {% component "unfold/components/title.html" %} + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns_30|default:0 }} + {% endcomponent %} + {% endcomponent %} + + {% component "unfold/components/card.html" %} + {% component "unfold/components/text.html" %} + {% trans "Processed orders (30d)" %} + {% endcomponent %} + {% component "unfold/components/title.html" %} + {{ processed_orders_30|default:0 }} + {% endcomponent %} + {% endcomponent %} +
diff --git a/engine/core/templates/admin/dashboard/_most_returned.html b/engine/core/templates/admin/dashboard/_most_returned.html new file mode 100644 index 00000000..d9514d5d --- /dev/null +++ b/engine/core/templates/admin/dashboard/_most_returned.html @@ -0,0 +1,27 @@ +{% load i18n unfold %} + +{% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Most returned products (30d)" %} + {% endcomponent %} + {% if most_returned_products %} + + {% else %} + {% component "unfold/components/text.html" %} + {% trans "No returns in the last 30 days." %} + {% endcomponent %} + {% endif %} +{% endcomponent %} diff --git a/engine/core/templates/admin/dashboard/_product_lists.html b/engine/core/templates/admin/dashboard/_product_lists.html new file mode 100644 index 00000000..a71b0095 --- /dev/null +++ b/engine/core/templates/admin/dashboard/_product_lists.html @@ -0,0 +1,71 @@ +{% load i18n unfold %} + +
+ {% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Most wished product" %} + {% endcomponent %} + {% if most_wished_products %} + + {% elif most_wished_product %} + + {% if most_wished_product.image %} + {{ most_wished_product.name }} + {% endif %} + {{ most_wished_product.name }} + + {% else %} + {% component "unfold/components/text.html" %} + {% trans "No data yet." %} + {% endcomponent %} + {% endif %} + {% endcomponent %} + + {% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Most popular product" %} + {% endcomponent %} + {% if most_popular_products %} + + {% elif most_popular_product %} + + {% if most_popular_product.image %} + {{ most_popular_product.name }} + {% endif %} + {{ most_popular_product.name }} + + {% else %} + {% component "unfold/components/text.html" %} + {% trans "No data yet." %} + {% endcomponent %} + {% endif %} + {% endcomponent %} +
diff --git a/engine/core/templates/admin/dashboard/_quick_links.html b/engine/core/templates/admin/dashboard/_quick_links.html new file mode 100644 index 00000000..8ed55eb4 --- /dev/null +++ b/engine/core/templates/admin/dashboard/_quick_links.html @@ -0,0 +1,15 @@ +{% load i18n unfold %} + +{% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Quick Links" %} + {% endcomponent %} + {% if quick_links %} + {% component "unfold/components/navigation.html" with class="flex flex-col gap-1" items=quick_links %} + {% endcomponent %} + {% else %} + {% component "unfold/components/text.html" %} + {% trans "No links available." %} + {% endcomponent %} + {% endif %} +{% endcomponent %} diff --git a/engine/core/templates/admin/dashboard/_shipped_vs_digital.html b/engine/core/templates/admin/dashboard/_shipped_vs_digital.html new file mode 100644 index 00000000..6998b32c --- /dev/null +++ b/engine/core/templates/admin/dashboard/_shipped_vs_digital.html @@ -0,0 +1,29 @@ +{% load i18n unfold %} + +{% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Shipped vs Digital (30d)" %} + {% endcomponent %} + +
+
+ {% trans "Digital" %} + + {{ shipped_vs_digital.digital_qty }} ({% firstof shipped_vs_digital.digital_pct 0 %}%) · + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ shipped_vs_digital.digital_gross }} + +
+ {% component "unfold/components/progress.html" with value=shipped_vs_digital.digital_pct title='' description='' %} + {% endcomponent %} + +
+ {% trans "Shipped" %} + + {{ shipped_vs_digital.shipped_qty }} ({% firstof shipped_vs_digital.shipped_pct 0 %}%) · + {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ shipped_vs_digital.shipped_gross }} + +
+ {% component "unfold/components/progress.html" with value=shipped_vs_digital.shipped_pct title='' description='' %} + {% endcomponent %} +
+{% endcomponent %} diff --git a/engine/core/templates/admin/dashboard/_top_categories.html b/engine/core/templates/admin/dashboard/_top_categories.html new file mode 100644 index 00000000..fe51b251 --- /dev/null +++ b/engine/core/templates/admin/dashboard/_top_categories.html @@ -0,0 +1,26 @@ +{% load i18n unfold %} + +{% component "unfold/components/card.html" %} + {% component "unfold/components/title.html" %} + {% trans "Top categories by quantity (30d)" %} + {% endcomponent %} + {% if top_categories %} + + {% else %} + {% component "unfold/components/text.html" %} + {% trans "No category sales in the last 30 days." %} + {% endcomponent %} + {% endif %} +{% endcomponent %} diff --git a/engine/core/templates/admin/index.html b/engine/core/templates/admin/index.html index 45b74158..92693e81 100644 --- a/engine/core/templates/admin/index.html +++ b/engine/core/templates/admin/index.html @@ -21,383 +21,28 @@
{% endcomponent %} -
- {% component "unfold/components/card.html" %} - {% component "unfold/components/text.html" %} - {% trans "Revenue (gross, 30d)" %} - {% endcomponent %} - {% component "unfold/components/title.html" %} - {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_gross_30|default:0 }} - {% endcomponent %} - {% endcomponent %} - - {% component "unfold/components/card.html" %} - {% component "unfold/components/text.html" %} - {% trans "Revenue (net, 30d)" %} - {% endcomponent %} - {% component "unfold/components/title.html" %} - {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_net_30|default:0 }} - {% endcomponent %} - {% endcomponent %} - - {% component "unfold/components/card.html" %} - {% component "unfold/components/text.html" %} - {% trans "Returns (30d)" %} - {% endcomponent %} - {% component "unfold/components/title.html" %} - {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns_30|default:0 }} - {% endcomponent %} - {% endcomponent %} - - {% component "unfold/components/card.html" %} - {% component "unfold/components/text.html" %} - {% trans "Processed orders (30d)" %} - {% endcomponent %} - {% component "unfold/components/title.html" %} - {{ processed_orders_30|default:0 }} - {% endcomponent %} - {% endcomponent %} -
+ {% include "admin/dashboard/_kpis.html" %}
- {% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 %} - {% with total=gross|add:returns %} - {% component "unfold/components/card.html" with class="xl:col-span-2" %} - {% component "unfold/components/title.html" %} - {% trans "Income overview" %} - {% endcomponent %} - {% if total and total > 0 %} - {% with net=revenue_net_30|default:0 %} - {% with tax_amt=gross|sub:net %} - {% with returns_capped=returns %} - {% if returns > gross %} - {% with returns_capped=gross %}{% endwith %} - {% endif %} - {% with tax_amt_pos=tax_amt %} - {% if tax_amt_pos < 0 %} - {% with tax_amt_pos=0 %}{% endwith %} - {% endif %} - {% with net_for_pie=gross|sub:tax_amt_pos|sub:returns_capped %} - {% if net_for_pie < 0 %} - {% with net_for_pie=0 %}{% endwith %} - {% endif %} - {% widthratio net_for_pie gross 360 as net_deg %} - {% widthratio tax_amt_pos gross 360 as tax_deg %} - {% widthratio returns_capped gross 360 as ret_deg %} - {% with net_end=net_deg %} - {% with tax_end=net_end|add:tax_deg %} - {% with ret_end=tax_end|add:ret_deg %} -
-
- -
-
-
- - {% trans "Net" %}: - {% if currency_symbol %} - {{ currency_symbol }}{% endif %}{{ net }} -
- {% if tax_amt_pos > 0 %} -
- - {% trans "Taxes" %}: - {% if currency_symbol %} - {{ currency_symbol }}{% endif %}{{ tax_amt_pos|floatformat:2 }} -
- {% endif %} -
- - {% trans "Returns" %}: - {% if currency_symbol %} - {{ currency_symbol }}{% endif %}{{ returns }} -
-
- - {% trans "Gross (pie total)" %}: - {% if currency_symbol %} - {{ currency_symbol }}{% endif %}{{ gross }} -
-
-
- - - {% endwith %} - {% endwith %} - {% endwith %} - {% endwith %} - {% endwith %} - {% endwith %} - {% endwith %} - {% endwith %} - {% else %} - {% component "unfold/components/text.html" %} - {% trans "Not enough data for chart yet." %} - {% endcomponent %} - {% endif %} - {% endcomponent %} - {% endwith %} - {% endwith %} - - {% component "unfold/components/card.html" %} - {% component "unfold/components/title.html" %} - {% trans "Quick Links" %} - {% endcomponent %} - {% if quick_links %} - {% component "unfold/components/navigation.html" with class="flex flex-col gap-1" items=quick_links %} - {% endcomponent %} - {% else %} - {% component "unfold/components/text.html" %} - {% trans "No links available." %} - {% endcomponent %} - {% endif %} - {% endcomponent %} + {% include "admin/dashboard/_income_overview.html" %} + {% include "admin/dashboard/_quick_links.html" %}
- {% component "unfold/components/card.html" %} - {% component "unfold/components/title.html" %} - {% trans "Daily sales (30d)" %} - {% endcomponent %} - {% if daily_labels and daily_labels|length > 0 %} -
- -
- - {{ daily_labels|json_script:"daily-labels" }} - {{ daily_orders|json_script:"daily-orders" }} - {{ daily_gross|json_script:"daily-gross" }} - - {% else %} - {% component "unfold/components/text.html" %} - {% trans "Not enough data for chart yet." %} - {% endcomponent %} - {% endif %} - {% endcomponent %} + {% include "admin/dashboard/_daily_sales.html" %}
- {% component "unfold/components/card.html" %} - {% component "unfold/components/title.html" %} - {% trans "Most wished product" %} - {% endcomponent %} - {% if most_wished_products %} - - {% elif most_wished_product %} - - {% if most_wished_product.image %} - {{ most_wished_product.name }} - {% endif %} - {{ most_wished_product.name }} - - {% else %} - {% component "unfold/components/text.html" %} - {% trans "No data yet." %} - {% endcomponent %} - {% endif %} - {% endcomponent %} - - {% component "unfold/components/card.html" %} - {% component "unfold/components/title.html" %} - {% trans "Most popular product" %} - {% endcomponent %} - {% if most_popular_products %} - - {% elif most_popular_product %} - - {% if most_popular_product.image %} - {{ most_popular_product.name }} - {% endif %} - {{ most_popular_product.name }} - - {% else %} - {% component "unfold/components/text.html" %} - {% trans "No data yet." %} - {% endcomponent %} - {% endif %} - {% endcomponent %} + {% include "admin/dashboard/_customers_mix.html" %} + {% if shipped_vs_digital.digital_qty|default:0 > 0 and shipped_vs_digital.shipped_qty|default:0 > 0 %} + {% include "admin/dashboard/_shipped_vs_digital.html" %} + {% endif %}
+
+ {% include "admin/dashboard/_most_returned.html" %} + {% include "admin/dashboard/_top_categories.html" %} +
+ + {% include "admin/dashboard/_product_lists.html" %} {% component "unfold/components/separator.html" %} {% endcomponent %} diff --git a/engine/core/utils/commerce.py b/engine/core/utils/commerce.py index dbfd8ff5..47fe83e9 100644 --- a/engine/core/utils/commerce.py +++ b/engine/core/utils/commerce.py @@ -1,12 +1,14 @@ from contextlib import suppress from datetime import date, timedelta +from typing import Any from constance import config from django.db.models import Count, F, QuerySet, Sum from django.db.models.functions import Coalesce, TruncDate +from django.urls import reverse from django.utils.timezone import now -from engine.core.models import Order, OrderProduct +from engine.core.models import Category, Order, OrderProduct, Product def get_period_order_products( @@ -110,3 +112,156 @@ def get_daily_gross_revenue(period: timedelta = timedelta(days=30)) -> dict[date if d: result[d] = total_f return result + + +def get_top_returned_products(period: timedelta = timedelta(days=30), limit: int = 10) -> list[dict[str, Any]]: + current = now() + period_start = current - period + qs = ( + OrderProduct.objects.filter( + status="RETURNED", + order__status="FINISHED", + order__buy_time__lte=current, + order__buy_time__gte=period_start, + product__isnull=False, + ) + .values("product") + .annotate( + returned_qty=Coalesce(Sum("quantity"), 0), + returned_amount=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0), + ) + .order_by("-returned_qty")[:limit] + ) + + result: list[dict[str, Any]] = [] + prod_ids = [row["product"] for row in qs if row.get("product")] + products = Product.objects.filter(pk__in=prod_ids) + product_by_id = {p.pk: p for p in products} + for row in qs: + pid = row.get("product") + if not pid or pid not in product_by_id: + continue + p = product_by_id[pid] + img = "" + with suppress(Exception): + img = p.images.first().image_url if p.images.exists() else "" # type: ignore [union-attr] + result.append( + { + "name": p.name, + "image": img, + "admin_url": reverse("admin:core_product_change", args=[p.pk]), + "count": int(row.get("returned_qty", 0) or 0), + "amount": round(float(row.get("returned_amount", 0.0) or 0.0), 2), + } + ) + return result + + +def get_customer_mix(period: timedelta = timedelta(days=30)) -> dict[str, int]: + current = now() + period_start = current - period + period_users = ( + Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=period_start, user__isnull=False) + .values_list("user_id", flat=True) + .distinct() + ) + if not period_users: + return {"new": 0, "returning": 0} + + lifetime_counts = ( + Order.objects.filter(status="FINISHED", user_id__in=period_users).values("user_id").annotate(c=Count("id")) + ) + new_cnt = 0 + ret_cnt = 0 + for row in lifetime_counts: + c = int(row.get("c", 0) or 0) + if c <= 1: + new_cnt += 1 + else: + ret_cnt += 1 + return {"new": new_cnt, "returning": ret_cnt} + + +def get_top_categories_by_qty(period: timedelta = timedelta(days=30), limit: int = 10) -> list[dict[str, Any]]: + current = now() + period_start = current - period + qs = ( + OrderProduct.objects.filter( + status="FINISHED", + order__status="FINISHED", + order__buy_time__lte=current, + order__buy_time__gte=period_start, + product__isnull=False, + product__category__isnull=False, + ) + .values("product__category") + .annotate( + qty=Coalesce(Sum("quantity"), 0), + gross=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0), + ) + .order_by("-qty", "-gross")[:limit] + ) + + cat_ids = [row["product__category"] for row in qs if row.get("product__category")] + cats = Category.objects.filter(pk__in=cat_ids) + cat_by_id = {c.pk: c for c in cats} + result: list[dict[str, Any]] = [] + for row in qs: + cid = row.get("product__category") + if not cid or cid not in cat_by_id: + continue + c = cat_by_id[cid] + result.append( + { + "name": c.name, + "admin_url": reverse("admin:core_category_change", args=[c.pk]), + "qty": int(row.get("qty", 0) or 0), + "gross": round(float(row.get("gross", 0.0) or 0.0), 2), + } + ) + return result + + +def get_shipped_vs_digital_mix(period: timedelta = timedelta(days=30)) -> dict[str, float | int]: + current = now() + period_start = current - period + qs = ( + OrderProduct.objects.filter( + status="FINISHED", + order__status="FINISHED", + order__buy_time__lte=current, + order__buy_time__gte=period_start, + product__isnull=False, + ) + .values("product__is_digital") + .annotate( + qty=Coalesce(Sum("quantity"), 0), + gross=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0), + ) + ) + + digital_qty = 0 + shipped_qty = 0 + digital_gross = 0.0 + shipped_gross = 0.0 + for row in qs: + is_digital = bool(row.get("product__is_digital")) + q = int(row.get("qty", 0) or 0) + g = row.get("gross", 0.0) or 0.0 + try: + g = float(g) + except (TypeError, ValueError): + g = 0.0 + if is_digital: + digital_qty += q + digital_gross += g + else: + shipped_qty += q + shipped_gross += g + + return { + "digital_qty": int(digital_qty), + "shipped_qty": int(shipped_qty), + "digital_gross": round(float(digital_gross), 2), + "shipped_gross": round(float(shipped_gross), 2), + } diff --git a/engine/core/views.py b/engine/core/views.py index aead8076..bb2cc86d 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -1,9 +1,9 @@ import logging -from datetime import timedelta import mimetypes import os import traceback from contextlib import suppress +from datetime import timedelta import requests from django.conf import settings @@ -18,8 +18,8 @@ from django.template import Context from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.http import urlsafe_base64_decode -from django.utils.translation import gettext_lazy as _ from django.utils.timezone import now as tz_now +from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_headers from django_ratelimit.decorators import ratelimit @@ -60,11 +60,15 @@ from engine.core.serializers import ( from engine.core.utils import get_project_parameters, is_url_safe from engine.core.utils.caching import web_cache from engine.core.utils.commerce import ( - get_returns, - get_revenue, - get_total_processed_orders, + get_customer_mix, get_daily_finished_orders_count, get_daily_gross_revenue, + get_returns, + get_revenue, + get_shipped_vs_digital_mix, + get_top_categories_by_qty, + get_top_returned_products, + get_total_processed_orders, ) from engine.core.utils.emailing import contact_us_email from engine.core.utils.languages import get_flag_by_language @@ -452,9 +456,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: } ) - # Single most wished product (backward compatibility) most_wished: dict[str, str | int | float | None] | None = None - # Top 10 most wished products most_wished_list: list[dict[str, str | int | float | None]] = [] with suppress(Exception): wished_qs = ( @@ -475,12 +477,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "admin_url": reverse("admin:core_product_change", args=[product.pk]), } - # Build top 10 list wished_top10 = list(wished_qs[:10]) if wished_top10: counts_map = {row["products"]: row["cnt"] for row in wished_top10 if row.get("products")} products = Product.objects.filter(pk__in=counts_map.keys()) - # Preserve order as in wished_top10 product_by_id = {p.pk: p for p in products} for row in wished_top10: pid = row.get("products") @@ -497,9 +497,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: } ) - # Daily stats for the last 30 days try: - # Build continuous date axis today = tz_now().date() days = 30 date_axis = [today - timedelta(days=i) for i in range(days - 1, -1, -1)] @@ -507,7 +505,6 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: orders_map = get_daily_finished_orders_count(timedelta(days=days)) gross_map = get_daily_gross_revenue(timedelta(days=days)) - # Labels in day-month format, e.g., 05 Nov labels = [d.strftime("%d %b") for d in date_axis] orders_series = [int(orders_map.get(d, 0) or 0) for d in date_axis] gross_series = [float(gross_map.get(d, 0.0) or 0.0) for d in date_axis] @@ -515,15 +512,20 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: context["daily_labels"] = labels context["daily_orders"] = orders_series context["daily_gross"] = gross_series - except Exception as e: # pragma: no cover - fail safe + except Exception as e: logger.warning("Failed to build daily stats: %s", e) context["daily_labels"] = [] context["daily_orders"] = [] context["daily_gross"] = [] + with suppress(Exception): + today = tz_now().date() + days = 30 + date_axis = [today - timedelta(days=i) for i in range(days - 1, -1, -1)] + context["daily_labels"] = [d.strftime("%d %b") for d in date_axis] + context["daily_orders"] = [0 for _ in date_axis] + context["daily_gross"] = [0.0 for _ in date_axis] - # Single most popular product (backward compatibility) most_popular: dict[str, str | int | float | None] | None = None - # Top 10 most popular products by quantity most_popular_list: list[dict[str, str | int | float | None]] = [] with suppress(Exception): popular_qs = ( @@ -563,6 +565,50 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: } ) + customers_mix: dict[str, int | float] = {"new": 0, "returning": 0, "new_pct": 0.0, "returning_pct": 0.0} + with suppress(Exception): + mix = get_customer_mix() + n = int(mix.get("new", 0)) + r = int(mix.get("returning", 0)) + t = max(n + r, 0) + new_pct = round((n / t * 100.0), 1) if t > 0 else 0.0 + ret_pct = round((r / t * 100.0), 1) if t > 0 else 0.0 + customers_mix = {"new": n, "returning": r, "new_pct": new_pct, "returning_pct": ret_pct, "total": t} + + shipped_vs_digital: dict[str, int | float] = { + "digital_qty": 0, + "shipped_qty": 0, + "digital_gross": 0.0, + "shipped_gross": 0.0, + "digital_pct": 0.0, + "shipped_pct": 0.0, + } + with suppress(Exception): + svd = get_shipped_vs_digital_mix() + dq = int(svd.get("digital_qty", 0)) + sq = int(svd.get("shipped_qty", 0)) + total_q = dq + sq + digital_pct = round((dq / total_q * 100.0), 1) if total_q > 0 else 0.0 + shipped_pct = round((sq / total_q * 100.0), 1) if total_q > 0 else 0.0 + shipped_vs_digital.update( + { + "digital_qty": dq, + "shipped_qty": sq, + "digital_gross": float(svd.get("digital_gross", 0.0) or 0.0), + "shipped_gross": float(svd.get("shipped_gross", 0.0) or 0.0), + "digital_pct": digital_pct, + "shipped_pct": shipped_pct, + } + ) + + most_returned_products: list[dict[str, str | int | float]] = [] + with suppress(Exception): + most_returned_products = get_top_returned_products() + + top_categories: list[dict[str, str | int | float]] = [] + with suppress(Exception): + top_categories = get_top_categories_by_qty() + context.update( { "custom_variable": "value", @@ -577,6 +623,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "most_wished_products": most_wished_list, "most_popular_products": most_popular_list, "currency_symbol": currency_symbol, + "customers_mix": customers_mix, + "shipped_vs_digital": shipped_vs_digital, + "most_returned_products": most_returned_products, + "top_categories": top_categories, } ) diff --git a/evibes/settings/jazzmin.py b/evibes/settings/jazzmin.py deleted file mode 100644 index 44539e91..00000000 --- a/evibes/settings/jazzmin.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Deprecated: Jazzmin settings (removed in favor of django-unfold). - -This file is intentionally left as a stub to avoid accidental imports. -If imported, raise an explicit error guiding developers to Unfold. -""" - -raise ImportError( - "Jazzmin configuration has been removed. Use django-unfold instead. " - "See evibes/settings/unfold.py and INSTALLED_APPS in evibes/settings/base.py." -)