diff --git a/engine/core/templates/admin/index.html b/engine/core/templates/admin/index.html new file mode 100644 index 00000000..71416016 --- /dev/null +++ b/engine/core/templates/admin/index.html @@ -0,0 +1,89 @@ +{% extends 'admin/base.html' %} + +{% load i18n %} + +{% block title %} + {% if subtitle %} + {{ subtitle }} | + {% endif %} + + {{ title }} | {{ site_title|default:_('Django site admin') }} +{% endblock %} + +{% block branding %} + {% include "unfold/helpers/site_branding.html" %} +{% endblock %} + +{% block content %} +
+

{% trans "Dashboard" %}

+ +
+
+
{% trans "Revenue (gross, 30d)" %}
+
{{ revenue_gross_30|default:0 }}
+
+
+
{% trans "Revenue (net, 30d)" %}
+
{{ revenue_net_30|default:0 }}
+
+
+
{% trans "Returns (30d)" %}
+
{{ returns_30|default:0 }}
+
+
+
{% trans "Processed orders (30d)" %}
+
{{ processed_orders_30|default:0 }}
+
+
+ +
+ {% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 total=gross|add:returns %} +
+
+

{% trans "Sales vs Returns (30d)" %}

+
+ {% if total and total > 0 %} + {% widthratio gross total 360 as gross_deg %} + {% widthratio returns total 360 as returns_deg %} +
+
+
+
+
+
+
+ + {% trans "Gross" %}: + {{ gross }} +
+
+ + {% trans "Returns" %}: + {{ returns }} +
+
+
+ {% else %} +

{% trans "Not enough data for chart yet." %}

+ {% endif %} +
+ {% endwith %} + + +
+
+{% endblock %} diff --git a/engine/core/utils/commerce.py b/engine/core/utils/commerce.py new file mode 100644 index 00000000..e8896396 --- /dev/null +++ b/engine/core/utils/commerce.py @@ -0,0 +1,52 @@ +from datetime import timedelta + +from django.db.models import F, Sum +from django.db.models.functions import Coalesce +from django.utils.timezone import now +from constance import config +from engine.core.models import Order, OrderProduct + + +def get_period_order_products(period: timedelta = timedelta(days=30), statuses: list[str] | None = None): + if statuses is None: + statuses = ["FINISHED"] + current = now() + perioded = current - period + orders = Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=perioded) + return OrderProduct.objects.filter(status__in=statuses, order__in=orders) + + +def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)): + order_products = get_period_order_products(period) + total: float = ( + order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0 + ) + try: + total = float(total) + except (TypeError, ValueError): + total = 0.0 + + if clear: + try: + tax_rate = float(config.TAX_RATE or 0) + except (TypeError, ValueError): + tax_rate = 0.0 + net = total * (1 - tax_rate / 100.0) + return round(net, 2) + else: + return round(float(total), 2) + + +def get_returns(period: timedelta = timedelta(days=30)): + order_products = get_period_order_products(period, ["RETURNED"]) + total_returns: float = ( + order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0 + ) + try: + return round(float(total_returns), 2) + except (TypeError, ValueError): + return 0.0 + + +def get_total_processed_orders(period: timedelta = timedelta(days=30)): + return get_period_order_products(period, ["RETURNED", "FINISHED"]).count() diff --git a/engine/core/views.py b/engine/core/views.py index 0602bd1b..286fde27 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -52,6 +52,7 @@ from engine.core.serializers import ( LanguageSerializer, ) from engine.core.utils import get_project_parameters, is_url_safe +from engine.core.utils.commerce import get_revenue, get_returns, get_total_processed_orders from engine.core.utils.caching import web_cache from engine.core.utils.emailing import contact_us_email from engine.core.utils.languages import get_flag_by_language @@ -410,3 +411,27 @@ def version(request: HttpRequest, *args, **kwargs) -> HttpResponse: version.__doc__ = _( # type: ignore [assignment] "Returns current version of the eVibes. " ) + + +def dashboard_callback(request, context): + revenue_gross_30 = get_revenue(clear=False) + revenue_net_30 = get_revenue(clear=True) + returns_30 = get_returns() + processed_orders_30 = get_total_processed_orders() + + context.update( + { + "custom_variable": "value", + "revenue_gross_30": revenue_gross_30, + "revenue_net_30": revenue_net_30, + "returns_30": returns_30, + "processed_orders_30": processed_orders_30, + } + ) + + return context + + +dashboard_callback.__doc__ = _( # type: ignore [assignment] + "Returns custom variables for Dashboard. " +) diff --git a/evibes/settings/base.py b/evibes/settings/base.py index ac687ea4..10b9ffd2 100644 --- a/evibes/settings/base.py +++ b/evibes/settings/base.py @@ -250,6 +250,37 @@ LANGUAGES: tuple[tuple[str, str], ...] = ( ("zh-hans", "įŽ€äŊ“中文"), ) +LANGUAGES_FLAGS: dict[str, str] = { + "ar-ar": "🇸đŸ‡Ļ", + "cs-cz": "🇨đŸ‡ŋ", + "da-dk": "🇩🇰", + "de-de": "🇩đŸ‡Ē", + "en-gb": "đŸ‡Ŧ🇧", + "en-us": "đŸ‡ē🇸", + "es-es": "đŸ‡Ē🇸", + "fa-ir": "🇮🇷", + "fr-fr": "đŸ‡Ģ🇷", + "he-il": "🇮🇱", + "hi-in": "đŸ‡ŽđŸ‡ŗ", + "hr-hr": "🇭🇷", + "id-id": "🇮🇩", + "it-it": "🇮🇹", + "ja-jp": "đŸ‡¯đŸ‡ĩ", + "kk-kz": "🇰đŸ‡ŋ", + "ko-kr": "🇰🇷", + "nl-nl": "đŸ‡ŗđŸ‡ą", + "no-no": "đŸ‡ŗđŸ‡´", + "pl-pl": "đŸ‡ĩ🇱", + "pt-br": "🇧🇷", + "ro-ro": "🇷🇴", + "ru-ru": "🇷đŸ‡ē", + "sv-se": "🇸đŸ‡Ē", + "th-th": "🇹🇭", + "tr-tr": "🇹🇷", + "vi-vn": "đŸ‡ģđŸ‡ŗ", + "zh-hans": "đŸ‡¨đŸ‡ŗ", +} + LANGUAGE_CODE: str = "en-gb" CURRENCIES_BY_LANGUAGES: tuple[tuple[str, str], ...] = ( diff --git a/evibes/settings/unfold.py b/evibes/settings/unfold.py index 17e74c26..ae8b2b0e 100644 --- a/evibes/settings/unfold.py +++ b/evibes/settings/unfold.py @@ -2,7 +2,14 @@ from django.templatetags.static import static from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from evibes.settings.base import PROJECT_NAME, STOREFRONT_DOMAIN, SUPPORT_CONTACT, TASKBOARD_URL +from evibes.settings.base import ( + PROJECT_NAME, + STOREFRONT_DOMAIN, + SUPPORT_CONTACT, + TASKBOARD_URL, + LANGUAGES as BASE_LANGUAGES, + LANGUAGES_FLAGS, +) UNFOLD = { "SITE_URL": STOREFRONT_DOMAIN, @@ -20,38 +27,22 @@ UNFOLD = { "search_models": True, "show_history": True, }, + "DASHBOARD_CALLBACK": "core.views.dashboard_callback", + "LANGUAGES": { + "navigation": [ + { + "bidi": code.split("-")[0] in {"ar", "he", "fa", "ur"}, + "code": code, + "name": LANGUAGES_FLAGS.get(code), + "name_local": name, + "name_translated": _(name), + } + for code, name in BASE_LANGUAGES + ], + }, "EXTENSIONS": { "modeltranslation": { - "flags": { - "ar-ar": "🇸đŸ‡Ļ", - "cs-cz": "🇨đŸ‡ŋ", - "da-dk": "🇩🇰", - "de-de": "🇩đŸ‡Ē", - "en-gb": "đŸ‡Ŧ🇧", - "en-us": "đŸ‡ē🇸", - "es-es": "đŸ‡Ē🇸", - "fa-ir": "🇮🇷", - "fr-fr": "đŸ‡Ģ🇷", - "he-il": "🇮🇱", - "hi-in": "đŸ‡ŽđŸ‡ŗ", - "hr-hr": "🇭🇷", - "id-id": "🇮🇩", - "it-it": "🇮🇹", - "ja-jp": "đŸ‡¯đŸ‡ĩ", - "kk-kz": "🇰đŸ‡ŋ", - "ko-kr": "🇰🇷", - "nl-nl": "đŸ‡ŗđŸ‡ą", - "no-no": "đŸ‡ŗđŸ‡´", - "pl-pl": "đŸ‡ĩ🇱", - "pt-br": "🇧🇷", - "ro-ro": "🇷🇴", - "ru-ru": "🇷đŸ‡ē", - "sv-se": "🇸đŸ‡Ē", - "th-th": "🇹🇭", - "tr-tr": "🇹🇷", - "vi-vn": "đŸ‡ģđŸ‡ŗ", - "zh-hans": "đŸ‡¨đŸ‡ŗ", - }, + "flags": LANGUAGES_FLAGS, }, }, "SIDEBAR": {