Merge branch 'main' into storefront-nuxt

This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-17 15:50:43 +03:00
commit 7907613fdb
5 changed files with 429 additions and 167 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% load i18n unfold %} {% load i18n unfold static %}
{% block title %} {% block title %}
{% if subtitle %} {% if subtitle %}
@ -16,184 +16,341 @@
{% block content %} {% block content %}
{% component "unfold/components/container.html" %} {% component "unfold/components/container.html" %}
<div class="flex flex-col min-h-screen"> <div class="flex flex-col min-h-screen">
{% component "unfold/components/title.html" %} {% component "unfold/components/title.html" %}
{% trans "Dashboard" %} {% trans "Dashboard" %}
<br/> <br/>
{% endcomponent %}
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
{% 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 %} {% endcomponent %}
{% component "unfold/components/card.html" %} <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
{% component "unfold/components/text.html" %} {% component "unfold/components/card.html" %}
{% trans "Revenue (net, 30d)" %} {% 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 %} {% 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/card.html" %}
{% component "unfold/components/text.html" %} {% component "unfold/components/text.html" %}
{% trans "Returns (30d)" %} {% trans "Revenue (net, 30d)" %}
{% endcomponent %}
{% component "unfold/components/title.html" %}
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_net_30|default:0 }}
{% endcomponent %}
{% endcomponent %} {% 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/card.html" %}
{% component "unfold/components/text.html" %} {% component "unfold/components/text.html" %}
{% trans "Processed orders (30d)" %} {% trans "Returns (30d)" %}
{% endcomponent %}
{% component "unfold/components/title.html" %}
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns_30|default:0 }}
{% endcomponent %}
{% endcomponent %} {% endcomponent %}
{% component "unfold/components/title.html" %}
{{ processed_orders_30|default:0 }}
{% endcomponent %}
{% endcomponent %}
</div>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 items-start"> {% component "unfold/components/card.html" %}
{% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 %} {% component "unfold/components/text.html" %}
{% with total=gross|add:returns %} {% trans "Processed orders (30d)" %}
{% component "unfold/components/card.html" with class="xl:col-span-2" %} {% endcomponent %}
{% component "unfold/components/title.html" %} {% component "unfold/components/title.html" %}
{% trans "Sales vs Returns (30d)" %} {{ processed_orders_30|default:0 }}
{% endcomponent %} {% endcomponent %}
{% if total and total > 0 %} {% endcomponent %}
{% widthratio gross total 360 as gross_deg %} </div>
<div class="flex flex-col sm:flex-row items-center gap-6">
<div class="relative w-40 h-40"> <div class="grid grid-cols-1 xl:grid-cols-3 gap-6 items-start">
<div class="w-40 h-40 rounded-full" {% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 %}
style="background: {% with total=gross|add:returns %}
conic-gradient( {% component "unfold/components/card.html" with class="xl:col-span-2" %}
rgb(34,197,94) 0 {{ gross_deg }}deg, {% component "unfold/components/title.html" %}
rgb(239,68,68) {{ gross_deg }}deg 360deg {% trans "Sales vs Returns (30d)" %}
);">
</div>
</div>
<div>
<div class="flex items-center gap-2 mb-2">
<span class="inline-block w-3 h-3 rounded-sm"
style="background:rgb(34,197,94)"></span>
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Gross" %}:</span>
<span class="font-semibold">{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ gross }}</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block w-3 h-3 rounded-sm"
style="background:rgb(239,68,68)"></span>
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Returns" %}:</span>
<span class="font-semibold">{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns }}</span>
</div>
</div>
</div>
{% else %}
{% component "unfold/components/text.html" %}
{% trans "Not enough data for chart yet." %}
{% endcomponent %} {% endcomponent %}
{% endif %} {% if total and total > 0 %}
{% endcomponent %} {% with net=revenue_net_30|default:0 %}
{% with tax_amt=gross|add:-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|add:-tax_amt_pos|add:-returns_capped %}
{% if net_for_pie < 0 %}
{% with net_for_pie=0 %}{% endwith %}
{% endif %}
{# Degrees per slice #}
{% 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 %}
<div class="flex flex-col sm:flex-row items-center gap-6">
<div class="relative w-40 h-40">
<div class="w-40 h-40 rounded-full"
style="background:
conic-gradient(
rgb(34,197,94) 0 {{ net_end }}deg,
rgb(249,115,22) {{ net_end }}deg {{ tax_end }}deg,
rgb(239,68,68) {{ tax_end }}deg 360deg
);">
</div>
</div>
<div>
<div class="flex items-center gap-2 mb-2">
<span class="inline-block w-3 h-3 rounded-sm"
style="background:rgb(34,197,94)"></span>
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Net" %}:</span>
<span class="font-semibold">{% if currency_symbol %}
{{ currency_symbol }}{% endif %}{{ net }}</span>
</div>
{% if tax_amt_pos > 0 %}
<div class="flex items-center gap-2 mb-2">
<span class="inline-block w-3 h-3 rounded-sm"
style="background:rgb(249,115,22)"></span>
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Taxes" %}:</span>
<span class="font-semibold">{% if currency_symbol %}
{{ currency_symbol }}{% endif %}{{ tax_amt_pos|floatformat:2 }}</span>
</div>
{% endif %}
<div class="flex items-center gap-2 mb-2">
<span class="inline-block w-3 h-3 rounded-sm"
style="background:rgb(239,68,68)"></span>
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Returns" %}:</span>
<span class="font-semibold">{% if currency_symbol %}
{{ currency_symbol }}{% endif %}{{ returns }}</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block w-3 h-3 rounded-sm"
style="background:linear-gradient(90deg, rgba(0,0,0,0.15), rgba(0,0,0,0.15))"></span>
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Gross (pie total)" %}:</span>
<span class="font-semibold">{% if currency_symbol %}
{{ currency_symbol }}{% endif %}{{ gross }}</span>
</div>
</div>
</div>
{% 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 %} {% 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 %}
</div>
{% component "unfold/components/card.html" %} {% component "unfold/components/card.html" %}
{% component "unfold/components/title.html" %} {% component "unfold/components/title.html" %}
{% trans "Quick Links" %} {% trans "Daily sales (30d)" %}
{% endcomponent %} {% endcomponent %}
{% if quick_links %} {% if daily_labels and daily_labels|length > 0 %}
{% component "unfold/components/navigation.html" with class="flex flex-col gap-1" items=quick_links %} <div class="w-full">
{% endcomponent %} <canvas id="dailySalesChart" height="120"></canvas>
{% else %} </div>
{% component "unfold/components/text.html" %} <script src="{% static 'js/chart.js' %}"></script>
{% trans "No links available." %} {{ daily_labels|json_script:"daily-labels" }}
{% endcomponent %} {{ daily_orders|json_script:"daily-orders" }}
{% endif %} {{ daily_gross|json_script:"daily-gross" }}
{% endcomponent %} <script>
</div> (function () {
try {
const labels = JSON.parse(document.getElementById('daily-labels').textContent);
const orders = JSON.parse(document.getElementById('daily-orders').textContent);
const gross = JSON.parse(document.getElementById('daily-gross').textContent);
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6"> const ctx = document.getElementById('dailySalesChart').getContext('2d');
{% component "unfold/components/card.html" %} const green = 'rgb(34,197,94)';
{% component "unfold/components/title.html" %} const blue = 'rgb(59,130,246)';
{% trans "Most wished product" %}
{% endcomponent %} const currency = "{% if currency_symbol %}{{ currency_symbol }}{% endif %}";
{% if most_wished_products %}
<ul class="flex flex-col divide-y divide-gray-200 dark:divide-base-700/50"> new Chart(ctx, {
{% for p in most_wished_products %} type: 'line',
<li class="py-2 first:pt-0 last:pb-0"> data: {
<a href="{{ p.admin_url }}" class="flex items-center gap-4"> labels: labels,
{% if p.image %} datasets: [
<img src="{{ p.image }}" alt="{{ p.name }}" class="w-12 h-12 object-cover rounded"/> {
{% endif %} label: '{{ _("Orders (FINISHED)") }}',
<span class="font-medium flex-1 truncate">{{ p.name }}</span> data: orders,
<span class="text-xs px-2 py-0.5 rounded bg-base-700/[.06] dark:bg-white/[.06] text-gray-700 dark:text-gray-200">{{ p.count }}</span> borderColor: green,
</a> backgroundColor: green,
</li> borderWidth: 2,
{% endfor %} tension: 0.25,
</ul> pointRadius: 2,
{% elif most_wished_product %} yAxisID: 'yOrders',
<a href="{{ most_wished_product.admin_url }}" class="flex items-center gap-4"> },
{% if most_wished_product.image %} {
<img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}" label: '{{ _("Gross revenue") }}',
class="w-16 h-16 object-cover rounded"/> data: gross,
{% endif %} borderColor: blue,
<span class="font-medium">{{ most_wished_product.name }}</span> backgroundColor: blue,
</a> borderWidth: 2,
tension: 0.25,
pointRadius: 2,
yAxisID: 'yRevenue',
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {mode: 'index', intersect: false},
plugins: {
legend: {display: true},
tooltip: {
callbacks: {
label: function (context) {
const label = context.dataset.label || '';
const val = context.parsed.y;
if (context.dataset.yAxisID === 'yRevenue') {
return `${label}: ${currency}${val}`;
}
return `${label}: ${val}`;
}
}
}
},
scales: {
x: {
grid: {display: false}
},
yOrders: {
type: 'linear',
position: 'left',
title: {display: true, text: '{{ _("Orders") }}'},
grid: {color: 'rgba(0,0,0,0.06)'},
ticks: {precision: 0}
},
yRevenue: {
type: 'linear',
position: 'right',
title: {display: true, text: '{{ _("Gross") }}'},
grid: {drawOnChartArea: false},
ticks: {
callback: function (value) {
return `${currency}${value}`;
}
}
}
}
}
});
} catch (e) {
console && console.warn && console.warn('Chart init failed', e);
}
})();
</script>
{% else %} {% else %}
{% component "unfold/components/text.html" %} {% component "unfold/components/text.html" %}
{% trans "No data yet." %} {% trans "Not enough data for chart yet." %}
{% endcomponent %} {% endcomponent %}
{% endif %} {% endif %}
{% endcomponent %} {% endcomponent %}
{% component "unfold/components/card.html" %} <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
{% component "unfold/components/title.html" %} {% component "unfold/components/card.html" %}
{% trans "Most popular product" %} {% component "unfold/components/title.html" %}
{% endcomponent %} {% trans "Most wished product" %}
{% if most_popular_products %}
<ul class="flex flex-col divide-y divide-gray-200 dark:divide-base-700/50">
{% for p in most_popular_products %}
<li class="py-2 first:pt-0 last:pb-0">
<a href="{{ p.admin_url }}" class="flex items-center gap-4">
{% if p.image %}
<img src="{{ p.image }}" alt="{{ p.name }}" class="w-12 h-12 object-cover rounded"/>
{% endif %}
<span class="font-medium flex-1 truncate">{{ p.name }}</span>
<span class="text-xs px-2 py-0.5 rounded bg-base-700/[.06] dark:bg-white/[.06] text-gray-700 dark:text-gray-200">{{ p.count }}</span>
</a>
</li>
{% endfor %}
</ul>
{% elif most_popular_product %}
<a href="{{ most_popular_product.admin_url }}" class="flex items-center gap-4">
{% if most_popular_product.image %}
<img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}"
class="w-16 h-16 object-cover rounded"/>
{% endif %}
<span class="font-medium">{{ most_popular_product.name }}</span>
</a>
{% else %}
{% component "unfold/components/text.html" %}
{% trans "No data yet." %}
{% endcomponent %} {% endcomponent %}
{% endif %} {% if most_wished_products %}
<ul class="flex flex-col divide-y divide-gray-200 dark:divide-base-700/50">
{% for p in most_wished_products %}
<li class="py-2 first:pt-0 last:pb-0">
<a href="{{ p.admin_url }}" class="flex items-center gap-4">
{% if p.image %}
<img src="{{ p.image }}" alt="{{ p.name }}"
class="w-12 h-12 object-cover rounded"/>
{% endif %}
<span class="font-medium flex-1 truncate">{{ p.name }}</span>
<span class="text-xs px-2 py-0.5 rounded bg-base-700/[.06] dark:bg-white/[.06] text-gray-700 dark:text-gray-200">{{ p.count }}</span>
</a>
</li>
{% endfor %}
</ul>
{% elif most_wished_product %}
<a href="{{ most_wished_product.admin_url }}" class="flex items-center gap-4">
{% if most_wished_product.image %}
<img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}"
class="w-16 h-16 object-cover rounded"/>
{% endif %}
<span class="font-medium">{{ most_wished_product.name }}</span>
</a>
{% 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 %}
<ul class="flex flex-col divide-y divide-gray-200 dark:divide-base-700/50">
{% for p in most_popular_products %}
<li class="py-2 first:pt-0 last:pb-0">
<a href="{{ p.admin_url }}" class="flex items-center gap-4">
{% if p.image %}
<img src="{{ p.image }}" alt="{{ p.name }}"
class="w-12 h-12 object-cover rounded"/>
{% endif %}
<span class="font-medium flex-1 truncate">{{ p.name }}</span>
<span class="text-xs px-2 py-0.5 rounded bg-base-700/[.06] dark:bg-white/[.06] text-gray-700 dark:text-gray-200">{{ p.count }}</span>
</a>
</li>
{% endfor %}
</ul>
{% elif most_popular_product %}
<a href="{{ most_popular_product.admin_url }}" class="flex items-center gap-4">
{% if most_popular_product.image %}
<img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}"
class="w-16 h-16 object-cover rounded"/>
{% endif %}
<span class="font-medium">{{ most_popular_product.name }}</span>
</a>
{% else %}
{% component "unfold/components/text.html" %}
{% trans "No data yet." %}
{% endcomponent %}
{% endif %}
{% endcomponent %}
</div>
{% component "unfold/components/separator.html" %}
{% endcomponent %} {% endcomponent %}
</div>
<div class="mt-4 mt-auto">
{% component "unfold/components/separator.html" %} {% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %}
{% endcomponent %} eVibes {{ evibes_version }} · Wiseless Team
{% endcomponent %}
<div class="mt-4 mt-auto"> </div>
{% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %}
eVibes {{ evibes_version }} · Wiseless Team
{% endcomponent %}
</div>
</div> </div>
{% endcomponent %} {% endcomponent %}

View file

@ -1,8 +1,9 @@
from datetime import timedelta from contextlib import suppress
from datetime import date, timedelta
from constance import config from constance import config
from django.db.models import F, QuerySet, Sum from django.db.models import Count, F, QuerySet, Sum
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce, TruncDate
from django.utils.timezone import now from django.utils.timezone import now
from engine.core.models import Order, OrderProduct from engine.core.models import Order, OrderProduct
@ -29,16 +30,32 @@ def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)) -> f
except (TypeError, ValueError): except (TypeError, ValueError):
total = 0.0 total = 0.0
if clear: if not 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) return round(float(total), 2)
try:
tax_rate = float(config.TAX_RATE or 0)
except (TypeError, ValueError):
tax_rate = 0.0
tax_included = False
with suppress(Exception):
tax_included = bool(getattr(config, "TAX_INCLUDED", False))
if tax_rate <= 0:
net = total
else:
if tax_included:
divisor = 1.0 + (tax_rate / 100.0)
if divisor <= 0:
net = total
else:
net = total / divisor
else:
net = total
return round(float(net or 0.0), 2)
def get_returns(period: timedelta = timedelta(days=30)) -> float: def get_returns(period: timedelta = timedelta(days=30)) -> float:
order_products = get_period_order_products(period, ["RETURNED"]) order_products = get_period_order_products(period, ["RETURNED"])
@ -53,3 +70,43 @@ def get_returns(period: timedelta = timedelta(days=30)) -> float:
def get_total_processed_orders(period: timedelta = timedelta(days=30)) -> int: def get_total_processed_orders(period: timedelta = timedelta(days=30)) -> int:
return get_period_order_products(period, ["RETURNED", "FINISHED"]).count() return get_period_order_products(period, ["RETURNED", "FINISHED"]).count()
def get_daily_finished_orders_count(period: timedelta = timedelta(days=30)) -> dict[date, int]:
current = now()
period_start = current - period
qs = (
Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=period_start)
.annotate(day=TruncDate("buy_time"))
.values("day")
.annotate(cnt=Count("id"))
.order_by("day")
)
result: dict[date, int] = {}
for row in qs:
d = row.get("day")
c = int(row.get("cnt", 0) or 0)
if d:
result[d] = c
return result
def get_daily_gross_revenue(period: timedelta = timedelta(days=30)) -> dict[date, float]:
qs = (
get_period_order_products(period, ["FINISHED"]) # OrderProduct queryset
.annotate(day=TruncDate("order__buy_time"))
.values("day")
.annotate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0))
.order_by("day")
)
result: dict[date, float] = {}
for row in qs:
d = row.get("day")
total = row.get("total") or 0.0
try:
total_f = round(float(total), 2)
except (TypeError, ValueError):
total_f = 0.0
if d:
result[d] = total_f
return result

View file

@ -1,4 +1,5 @@
import logging import logging
from datetime import timedelta
import mimetypes import mimetypes
import os import os
import traceback import traceback
@ -18,6 +19,7 @@ from django.urls import reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.timezone import now as tz_now
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers from django.views.decorators.vary import vary_on_headers
from django_ratelimit.decorators import ratelimit from django_ratelimit.decorators import ratelimit
@ -57,7 +59,13 @@ from engine.core.serializers import (
) )
from engine.core.utils import get_project_parameters, is_url_safe from engine.core.utils import get_project_parameters, is_url_safe
from engine.core.utils.caching import web_cache from engine.core.utils.caching import web_cache
from engine.core.utils.commerce import get_returns, get_revenue, get_total_processed_orders from engine.core.utils.commerce import (
get_returns,
get_revenue,
get_total_processed_orders,
get_daily_finished_orders_count,
get_daily_gross_revenue,
)
from engine.core.utils.emailing import contact_us_email from engine.core.utils.emailing import contact_us_email
from engine.core.utils.languages import get_flag_by_language from engine.core.utils.languages import get_flag_by_language
from engine.payments.serializers import TransactionProcessSerializer from engine.payments.serializers import TransactionProcessSerializer
@ -489,6 +497,30 @@ 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)]
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]
context["daily_labels"] = labels
context["daily_orders"] = orders_series
context["daily_gross"] = gross_series
except Exception as e: # pragma: no cover - fail safe
logger.warning("Failed to build daily stats: %s", e)
context["daily_labels"] = []
context["daily_orders"] = []
context["daily_gross"] = []
# Single most popular product (backward compatibility) # Single most popular product (backward compatibility)
most_popular: dict[str, str | int | float | None] | None = None most_popular: dict[str, str | int | float | None] | None = None
# Top 10 most popular products by quantity # Top 10 most popular products by quantity

View file

@ -25,6 +25,7 @@ CONSTANCE_CONFIG = OrderedDict(
("COMPANY_ADDRESS", (getenv("COMPANY_ADDRESS"), _("Address of the company"))), ("COMPANY_ADDRESS", (getenv("COMPANY_ADDRESS"), _("Address of the company"))),
("COMPANY_PHONE_NUMBER", (getenv("COMPANY_PHONE_NUMBER"), _("Phone number of the company"))), ("COMPANY_PHONE_NUMBER", (getenv("COMPANY_PHONE_NUMBER"), _("Phone number of the company"))),
("TAX_RATE", (0, _("Tax rate in jurisdiction of your company. Leave 0 if you don't want to process taxes."))), ("TAX_RATE", (0, _("Tax rate in jurisdiction of your company. Leave 0 if you don't want to process taxes."))),
("TAX_INCLUDED", (True, _("Shows if the taxes are already included in product's selling prices"))),
("EXCHANGE_RATE_API_KEY", (getenv("EXCHANGE_RATE_API_KEY", "example token"), _("Exchange rate API key"))), ("EXCHANGE_RATE_API_KEY", (getenv("EXCHANGE_RATE_API_KEY", "example token"), _("Exchange rate API key"))),
### Email Options ### ### Email Options ###
("EMAIL_BACKEND", ("django.core.mail.backends.smtp.EmailBackend", _("!!!DO NOT CHANGE!!!"))), ("EMAIL_BACKEND", ("django.core.mail.backends.smtp.EmailBackend", _("!!!DO NOT CHANGE!!!"))),
@ -58,6 +59,7 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict(
"COMPANY_ADDRESS", "COMPANY_ADDRESS",
"COMPANY_PHONE_NUMBER", "COMPANY_PHONE_NUMBER",
"TAX_RATE", "TAX_RATE",
"TAX_INCLUDED",
"EXCHANGE_RATE_API_KEY", "EXCHANGE_RATE_API_KEY",
), ),
_("Email Options"): ( _("Email Options"): (