Merge branch 'main' into storefront-nuxt

This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-16 16:26:16 +03:00
commit 57e5e49059
266 changed files with 17645 additions and 14124 deletions

View file

@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS build
WORKDIR /app
ARG EVIBES_BASE_DOMAIN
ARG EVIBES_PROJECT_NAME
ENV EVIBES_BASE_DOMAIN=$EVIBES_BASE_DOMAIN
ENV EVIBES_PROJECT_NAME=$EVIBES_PROJECT_NAME
COPY ./supervisor/package.json ./supervisor/package-lock.json ./
RUN npm ci --include=optional
COPY ./supervisor ./
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV HOST=0.0.0.0
ENV PORT=7777
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
RUN addgroup --system --gid 1001 nodeapp \
&& adduser --system --uid 1001 --ingroup nodeapp --home /home/nodeapp nodeapp
USER nodeapp
COPY --from=build /app/.output/ ./
RUN install -d -m 0755 -o nodeapp -g nodeapp /home/nodeapp \
&& printf '#!/bin/sh\nif [ \"$DEBUG\" = \"1\" ]; then export NODE_ENV=development; else export NODE_ENV=production; fi\nexec node /app/server/index.mjs\n' > /home/nodeapp/start.sh \
&& chown nodeapp:nodeapp /home/nodeapp/start.sh \
&& chmod +x /home/nodeapp/start.sh
USER nodeapp
CMD ["sh", "/home/nodeapp/start.sh"]

View file

@ -1,10 +1,10 @@
from django.contrib.admin import ModelAdmin, register
from django.contrib.admin import register
from django_summernote.admin import SummernoteModelAdminMixin
from unfold.admin import ModelAdmin
from engine.blog.models import Post, PostTag
from engine.core.admin import ActivationActionsMixin, FieldsetsMixin
from .models import Post, PostTag
@register(Post)
class PostAdmin(SummernoteModelAdminMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"

View file

@ -5,16 +5,33 @@ from constance.admin import Config
from constance.admin import ConstanceAdmin as BaseConstanceAdmin
from django.apps import AppConfig, apps
from django.conf import settings
from django.contrib.admin import ModelAdmin, TabularInline, action, register, site
from django.contrib.admin import register, site
from django.contrib.gis.admin import GISModelAdmin
from django.contrib.messages import constants as messages
from django.db.models import Model
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin
from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin
from django_celery_beat.admin import PeriodicTaskAdmin as BasePeriodicTaskAdmin
from django_celery_beat.admin import PeriodicTaskForm, TaskSelectWidget
from django_celery_beat.models import (
ClockedSchedule,
CrontabSchedule,
IntervalSchedule,
PeriodicTask,
SolarSchedule,
)
from djangoql.admin import DjangoQLSearchMixin
from import_export.admin import ImportExportModelAdmin
from modeltranslation.translator import NotRegistered, translator
from modeltranslation.utils import get_translation_fields
from mptt.admin import DraggableMPTTAdmin
from unfold.admin import ModelAdmin, TabularInline
from unfold.contrib.import_export.forms import ExportForm, ImportForm
from unfold.decorators import action
from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget
from engine.core.forms import CRMForm, OrderForm, OrderProductForm, StockForm, VendorForm
from engine.core.models import (
@ -65,15 +82,15 @@ class FieldsetsMixin:
for orig in transoptions.local_fields:
translation_fields += get_translation_fields(orig)
if translation_fields:
fss = list(fss) + [(_("translations"), {"fields": translation_fields})] # type: ignore [list-item]
fss = list(fss) + [(_("translations"), {"classes": ["tab"], "fields": translation_fields})] # type: ignore [list-item]
return fss
if self.general_fields:
fieldsets.append((_("general"), {"fields": self.general_fields}))
fieldsets.append((_("general"), {"classes": ["tab"], "fields": self.general_fields}))
if self.relation_fields:
fieldsets.append((_("relations"), {"fields": self.relation_fields}))
fieldsets.append((_("relations"), {"classes": ["tab"], "fields": self.relation_fields}))
if self.additional_fields:
fieldsets.append((_("additional info"), {"fields": self.additional_fields}))
fieldsets.append((_("additional info"), {"classes": ["tab"], "fields": self.additional_fields}))
opts = self.model._meta
meta_fields = []
@ -91,14 +108,14 @@ class FieldsetsMixin:
meta_fields.append("human_readable_id")
if meta_fields:
fieldsets.append((_("metadata"), {"fields": meta_fields}))
fieldsets.append((_("metadata"), {"classes": ["tab"], "fields": meta_fields}))
ts = []
for name in ("created", "modified"):
if any(f.name == name for f in opts.fields):
ts.append(name)
if ts:
fieldsets.append((_("timestamps"), {"fields": ts, "classes": ["collapse"]}))
fieldsets.append((_("timestamps"), {"classes": ["tab"], "fields": ts}))
fieldsets = add_translations_fieldset(fieldsets) # type: ignore [arg-type, assignment]
return fieldsets # type: ignore [return-value]
@ -140,10 +157,9 @@ class AttributeValueInline(TabularInline): # type: ignore [type-arg]
model = AttributeValue
extra = 0
autocomplete_fields = ["attribute"]
is_navtab = True
verbose_name = _("attribute value")
verbose_name_plural = _("attribute values")
icon = "fa-solid fa-list-ul"
tab = True
def get_queryset(self, request):
return super().get_queryset(request).select_related("attribute", "product")
@ -152,10 +168,9 @@ class AttributeValueInline(TabularInline): # type: ignore [type-arg]
class ProductImageInline(TabularInline): # type: ignore [type-arg]
model = ProductImage
extra = 0
is_navtab = True
tab = True
verbose_name = _("image")
verbose_name_plural = _("images")
icon = "fa-regular fa-images"
def get_queryset(self, request):
return super().get_queryset(request).select_related("product")
@ -165,10 +180,9 @@ class StockInline(TabularInline): # type: ignore [type-arg]
model = Stock
extra = 0
form = StockForm
is_navtab = True
tab = True
verbose_name = _("stock")
verbose_name_plural = _("stocks")
icon = "fa-solid fa-boxes-stacked"
def get_queryset(self, request):
return super().get_queryset(request).select_related("vendor", "product")
@ -179,10 +193,9 @@ class OrderProductInline(TabularInline): # type: ignore [type-arg]
extra = 0
readonly_fields = ("product", "quantity", "buy_price")
form = OrderProductForm
is_navtab = True
verbose_name = _("order product")
verbose_name_plural = _("order products")
icon = "fa-solid fa-boxes-packing"
tab = True
def get_queryset(self, request):
return super().get_queryset(request).select_related("product").only("product__name")
@ -193,14 +206,13 @@ class CategoryChildrenInline(TabularInline): # type: ignore [type-arg]
fk_name = "parent"
extra = 0
fields = ("name", "description", "is_active", "image", "markup_percent")
is_navtab = True
tab = True
verbose_name = _("children")
verbose_name_plural = _("children")
icon = "fa-solid fa-leaf"
@register(AttributeGroup)
class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class AttributeGroupAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = AttributeGroup # type: ignore [misc]
list_display = (
@ -224,7 +236,7 @@ class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
@register(Attribute)
class AttributeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class AttributeAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Attribute # type: ignore [misc]
list_display = (
@ -299,7 +311,7 @@ class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
@register(Category)
class CategoryAdmin(FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin):
class CategoryAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin, ModelAdmin):
# noinspection PyClassVar
model = Category
list_display = (
@ -348,7 +360,7 @@ class CategoryAdmin(FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin):
@register(Brand)
class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class BrandAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Brand # type: ignore [misc]
list_display = (
@ -383,7 +395,7 @@ class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: i
@register(Product)
class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class ProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin, ImportExportModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Product # type: ignore [misc]
list_display = (
@ -432,6 +444,8 @@ class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type:
ProductImageInline,
StockInline,
]
import_form_class = ImportForm
export_form_class = ExportForm
general_fields = [
"is_active",
@ -460,7 +474,7 @@ class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type:
@register(ProductTag)
class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class ProductTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = ProductTag # type: ignore [misc]
list_display = ("tag_name",)
@ -478,7 +492,7 @@ class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # ty
@register(CategoryTag)
class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class CategoryTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = CategoryTag # type: ignore [misc]
list_display = (
@ -504,7 +518,7 @@ class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # t
@register(Vendor)
class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class VendorAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Vendor # type: ignore [misc]
list_display = (
@ -544,7 +558,7 @@ class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type:
@register(Feedback)
class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class FeedbackAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Feedback # type: ignore [misc]
list_display = (
@ -577,7 +591,7 @@ class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type
@register(Order)
class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class OrderAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Order # type: ignore [misc]
list_display = (
@ -628,7 +642,7 @@ class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: i
@register(OrderProduct)
class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class OrderProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = OrderProduct # type: ignore [misc]
list_display = (
@ -666,7 +680,7 @@ class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): #
@register(PromoCode)
class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class PromoCodeAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = PromoCode # type: ignore [misc]
list_display = (
@ -710,7 +724,7 @@ class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # typ
@register(Promotion)
class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class PromotionAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Promotion # type: ignore [misc]
list_display = (
@ -737,7 +751,7 @@ class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # typ
@register(Stock)
class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class StockAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Stock # type: ignore [misc]
form = StockForm
@ -785,7 +799,7 @@ class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: i
@register(Wishlist)
class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class WishlistAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = Wishlist # type: ignore [misc]
list_display = (
@ -811,7 +825,7 @@ class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type
@register(ProductImage)
class ProductImageAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class ProductImageAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = ProductImage # type: ignore [misc]
list_display = (
@ -847,7 +861,7 @@ class ProductImageAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): #
@register(Address)
class AddressAdmin(FieldsetsMixin, GISModelAdmin): # type: ignore [misc]
class AddressAdmin(DjangoQLSearchMixin, FieldsetsMixin, GISModelAdmin): # type: ignore [misc]
# noinspection PyClassVar
model = Address # type: ignore [misc]
list_display = (
@ -897,7 +911,7 @@ class AddressAdmin(FieldsetsMixin, GISModelAdmin): # type: ignore [misc]
@register(CustomerRelationshipManagementProvider)
class CustomerRelationshipManagementProviderAdmin(FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class CustomerRelationshipManagementProviderAdmin(DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = CustomerRelationshipManagementProvider # type: ignore [misc]
list_display = (
@ -926,7 +940,7 @@ class CustomerRelationshipManagementProviderAdmin(FieldsetsMixin, ModelAdmin):
@register(OrderCrmLink)
class OrderCrmLinkAdmin(FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
class OrderCrmLinkAdmin(DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
# noinspection PyClassVar
model = OrderCrmLink # type: ignore [misc]
list_display = (
@ -993,6 +1007,49 @@ class ConstanceConfig:
site.unregister([Config])
# noinspection PyTypeChecker
site.register([ConstanceConfig], BaseConstanceAdmin) # type: ignore [list-item]
site.site_title = settings.CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]
site.site_title = settings.PROJECT_NAME
site.site_header = "eVibes"
site.index_title = settings.CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]
site.index_title = settings.PROJECT_NAME
site.unregister(PeriodicTask)
site.unregister(IntervalSchedule)
site.unregister(CrontabSchedule)
site.unregister(SolarSchedule)
site.unregister(ClockedSchedule)
class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget):
pass
class UnfoldPeriodicTaskForm(PeriodicTaskForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["task"].widget = UnfoldAdminTextInputWidget()
self.fields["regtask"].widget = UnfoldTaskSelectWidget()
@register(PeriodicTask)
class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
form = UnfoldPeriodicTaskForm
@register(IntervalSchedule)
class IntervalScheduleAdmin(ModelAdmin):
pass
@register(CrontabSchedule)
class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin):
pass
@register(SolarSchedule)
class SolarScheduleAdmin(ModelAdmin):
pass
@register(ClockedSchedule)
class ClockedScheduleAdmin(BaseClockedScheduleAdmin, ModelAdmin):
pass

View file

@ -2,7 +2,6 @@ import logging
from contextlib import suppress
from typing import Any
from constance import config
from django.conf import settings
from django.core.cache import cache
from django.db.models import Max, Min, QuerySet
@ -139,7 +138,7 @@ class BrandType(DjangoObjectType): # type: ignore [misc]
lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/brand/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
title = f"{self.name} | {settings.PROJECT_NAME}"
description = (self.description or "")[:180]
logo_url = None
@ -265,7 +264,7 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/catalog/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
title = f"{self.name} | {settings.PROJECT_NAME}"
description = (self.description or "")[:180]
og_image = graphene_abs(info.context, self.image.url) if getattr(self, "image", None) else ""
@ -537,7 +536,7 @@ class ProductType(DjangoObjectType): # type: ignore [misc]
lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/product/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
title = f"{self.name} | {settings.PROJECT_NAME}"
description = (self.description or "")[:180]
first_img = self.images.order_by("priority").first()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#e10098"><style>svg{fill:color(display-p3 0.8824 0 0.5961);}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M50 6.90308L87.323 28.4515V71.5484L50 93.0968L12.677 71.5484V28.4515L50 6.90308ZM16.8647 30.8693V62.5251L44.2795 15.0414L16.8647 30.8693ZM50 13.5086L18.3975 68.2457H81.6025L50 13.5086ZM77.4148 72.4334H22.5852L50 88.2613L77.4148 72.4334ZM83.1353 62.5251L55.7205 15.0414L83.1353 30.8693V62.5251Z"/><circle cx="50" cy="9.3209" r="8.82"/><circle cx="85.2292" cy="29.6605" r="8.82"/><circle cx="85.2292" cy="70.3396" r="8.82"/><circle cx="50" cy="90.6791" r="8.82"/><circle cx="14.7659" cy="70.3396" r="8.82"/><circle cx="14.7659" cy="29.6605" r="8.82"/></svg>

After

Width:  |  Height:  |  Size: 740 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
<path d="M0 0 C4.85630418 -0.09938127 9.71200765 -0.17170794 14.5690918 -0.21972656 C16.21893637 -0.23975797 17.8687107 -0.26699142 19.51831055 -0.30175781 C21.89910274 -0.35065357 24.278951 -0.3730528 26.66015625 -0.390625 C27.38960953 -0.41127014 28.11906281 -0.43191528 28.87062073 -0.45318604 C34.98996246 -0.45582643 39.16046149 1.39602858 43.7421875 5.59765625 C48.39070216 11.60866658 48.56284028 16.60873893 48 24 C46.25230248 29.44870404 42.60721838 32.78541808 38 36 C34.30459598 37.23180134 30.89696622 37.12412112 27.05078125 37.09765625 C26.27816452 37.0962413 25.50554779 37.09482635 24.70951843 37.09336853 C22.24380611 37.08777466 19.77818485 37.07522333 17.3125 37.0625 C15.63997527 37.05748421 13.96744912 37.05292144 12.29492188 37.04882812 C8.19657954 37.03780724 4.09830521 37.02054684 0 37 C0.33 36.01 0.66 35.02 1 34 C2.97285682 33.30950011 4.94642158 32.62085067 6.92578125 31.94921875 C10.37674801 30.36996277 11.86671615 28.10295833 14 25 C15.56840852 20.29477445 15.54895539 16.47351184 13.8125 11.8125 C10.19941703 6.20599194 6.23939638 4.46809327 0 3 C0 2.01 0 1.02 0 0 Z " fill="#ffffff" transform="translate(8,1)"/>
<path d="M0 0 C4.69557647 -0.09954745 9.39052663 -0.17178467 14.08691406 -0.21972656 C15.68142408 -0.23973053 17.27586178 -0.26694301 18.87011719 -0.30175781 C36.87926136 -0.68493109 36.87926136 -0.68493109 44.9375 6.6875 C47 10 47 10 48 19 C37.44 19 26.88 19 16 19 C15.01 16.03 14.02 13.06 13 10 C9.007668 5.73546355 5.53762935 4.21827846 0 3 C0 2.01 0 1.02 0 0 Z " fill="#ffffff" transform="translate(8,44)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_swagger</title><path d="M30,16a14,14,0,1,1-4.1-9.9A13.956,13.956,0,0,1,30,16Z" style="fill:#fff"/><path d="M27.9,16a11.9,11.9,0,1,1-3.485-8.415A11.863,11.863,0,0,1,27.9,16Z" style="fill:#6d9a00"/><path d="M11.66,15.983a.938.938,0,0,1,.977-.976.976.976,0,1,1-.977.976Z" style="fill:#fff"/><path d="M15.031,15.983a.938.938,0,0,1,.977-.976.976.976,0,1,1-.977.976Z" style="fill:#fff"/><path d="M18.4,15.983a.938.938,0,0,1,.977-.976.976.976,0,1,1-.977.976Z" style="fill:#fff"/><path d="M7.619,16.89V15.142A2.824,2.824,0,0,0,8.5,15a1.126,1.126,0,0,0,.439-.441,2.1,2.1,0,0,0,.254-.776,9.08,9.08,0,0,0,.055-1.216,10.547,10.547,0,0,1,.123-1.97,1.847,1.847,0,0,1,.446-.9,1.72,1.72,0,0,1,.81-.552,4.788,4.788,0,0,1,1.316-.131h.363v1.437a3.177,3.177,0,0,0-.977.091.63.63,0,0,0-.319.277,3.372,3.372,0,0,0-.1.941q0,.459-.062,1.741a4.639,4.639,0,0,1-.178,1.169,2.435,2.435,0,0,1-.367.739,2.939,2.939,0,0,1-.682.6,2.432,2.432,0,0,1,.662.579,2.377,2.377,0,0,1,.394.8,5.8,5.8,0,0,1,.178,1.267q.048,1.209.048,1.544a3.034,3.034,0,0,0,.11.932.694.694,0,0,0,.333.288,2.927,2.927,0,0,0,.963.1v1.486h-.363a3.843,3.843,0,0,1-1.292-.192A1.905,1.905,0,0,1,9.82,22.3a1.875,1.875,0,0,1-.456-.9,8.724,8.724,0,0,1-.117-1.686,8.414,8.414,0,0,0-.11-1.741,1.553,1.553,0,0,0-.456-.834A2.106,2.106,0,0,0,7.619,16.89Z" style="fill:#fff"/><path d="M23.285,17.143a1.553,1.553,0,0,0-.456.834,8.414,8.414,0,0,0-.11,1.741A8.724,8.724,0,0,1,22.6,21.4a1.875,1.875,0,0,1-.456.9,1.905,1.905,0,0,1-.833.521,3.843,3.843,0,0,1-1.292.192h-.363V21.53a2.927,2.927,0,0,0,.963-.1.694.694,0,0,0,.333-.288,3.034,3.034,0,0,0,.11-.932q0-.335.048-1.544A5.8,5.8,0,0,1,21.29,17.4a2.377,2.377,0,0,1,.394-.8,2.432,2.432,0,0,1,.662-.579,2.939,2.939,0,0,1-.682-.6,2.435,2.435,0,0,1-.367-.739,4.639,4.639,0,0,1-.178-1.169q-.062-1.282-.062-1.741a3.372,3.372,0,0,0-.1-.941.63.63,0,0,0-.319-.277,3.177,3.177,0,0,0-.977-.091V9.016h.363a4.788,4.788,0,0,1,1.316.131,1.72,1.72,0,0,1,.81.552,1.847,1.847,0,0,1,.446.9,10.547,10.547,0,0,1,.123,1.97,9.08,9.08,0,0,0,.055,1.216,2.1,2.1,0,0,0,.254.776,1.126,1.126,0,0,0,.439.441,2.824,2.824,0,0,0,.883.144V16.89A2.106,2.106,0,0,0,23.285,17.143Z" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,167 @@
{% extends 'admin/base.html' %}
{% load i18n unfold %}
{% 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 %}
{% component "unfold/components/container.html" %}
{% component "unfold/components/title.html" %}
{% trans "Dashboard" %}
<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" %}
{{ 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" %}
{{ 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" %}
{{ 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 %}
</div>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 items-start">
{% 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 "Sales vs Returns (30d)" %}
{% endcomponent %}
{% if total and total > 0 %}
{% widthratio gross total 360 as gross_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 {{ gross_deg }}deg,
rgb(239,68,68) {{ gross_deg }}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 "Gross" %}:</span>
<span class="font-semibold">{{ 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">{{ returns }}</span>
</div>
</div>
</div>
{% 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 %}
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
{% component "unfold/components/card.html" %}
{% component "unfold/components/title.html" %}
{% trans "Most wished product" %}
{% endcomponent %}
{% if most_wished_product %}
<a href="{{ most_wished_product.admin_url }}" class="flex items-center gap-4">
<img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}"
class="w-16 h-16 object-cover rounded"/>
<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_product %}
<a href="{{ most_popular_product.admin_url }}" class="flex items-center gap-4">
<img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}"
class="w-16 h-16 object-cover rounded"/>
<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 %}
<div class="mt-4">
<br/>
{% 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>
{% endcomponent %}
{% endblock %}

View file

@ -30,4 +30,5 @@ class DRFCoreViewsTests(TestCase):
serializer.is_valid(raise_exception=True)
return serializer.validated_data["access_token"]
# TODO: create tests for every possible HTTP method in core module with DRF stack

View file

@ -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()

View file

@ -24,7 +24,7 @@ def contact_us_email(contact_info) -> tuple[bool, str]:
)
email = EmailMessage(
_(f"{config.PROJECT_NAME} | contact us initiated"),
_(f"{settings.PROJECT_NAME} | contact us initiated"),
render_to_string(
"../templates/contact_us_email.html",
{
@ -37,7 +37,7 @@ def contact_us_email(contact_info) -> tuple[bool, str]:
},
),
to=[config.EMAIL_FROM],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"
@ -70,7 +70,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
if not order.is_whole_digital:
email = EmailMessage(
_(f"{config.PROJECT_NAME} | order confirmation"),
_(f"{settings.PROJECT_NAME} | order confirmation"),
render_to_string(
"digital_order_created_email.html" if order.is_whole_digital else "shipped_order_created_email.html",
{
@ -81,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
},
),
to=[recipient],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"
@ -102,14 +102,14 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
activate(order.user.language)
email = EmailMessage(
_(f"{config.PROJECT_NAME} | order delivered"),
_(f"{settings.PROJECT_NAME} | order delivered"),
render_to_string(
template_name="../templates/digital_order_delivered_email.html",
context={
"order_uuid": order.human_readable_id,
"user_first_name": "" or order.user.first_name,
"order_products": ops,
"project_name": config.PROJECT_NAME,
"project_name": settings.PROJECT_NAME,
"contact_email": config.EMAIL_FROM,
"total_price": round(sum(0.0 or op.buy_price for op in ops), 2), # type: ignore [misc]
"display_system_attributes": order.user.has_perm("core.view_order"),
@ -117,7 +117,7 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
},
),
to=[order.user.email],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"
@ -185,20 +185,20 @@ def send_promocode_created_email(promocode_pk: str) -> tuple[bool, str]:
activate(promocode.user.language)
email = EmailMessage(
_(f"{config.PROJECT_NAME} | promocode granted"),
_(f"{settings.PROJECT_NAME} | promocode granted"),
render_to_string(
template_name="../templates/promocode_granted_email.html",
context={
"promocode": promocode,
"user_first_name": "" or promocode.user.first_name,
"project_name": config.PROJECT_NAME,
"project_name": settings.PROJECT_NAME,
"contact_email": config.EMAIL_FROM,
"today": datetime.today(),
"currency": settings.CURRENCY_CODE,
},
),
to=[promocode.user.email],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"

View file

@ -18,7 +18,7 @@ def website_schema():
return {
"@context": "https://schema.org",
"@type": "WebSite",
"name": config.PROJECT_NAME,
"name": settings.PROJECT_NAME,
"url": f"https://{settings.BASE_DOMAIN}/",
"potentialAction": {
"@type": "SearchAction",

View file

@ -2,6 +2,7 @@ import logging
import mimetypes
import os
import traceback
from contextlib import suppress
import requests
from django.conf import settings
@ -9,8 +10,10 @@ from django.contrib.sitemaps.views import index as _sitemap_index_view
from django.contrib.sitemaps.views import sitemap as _sitemap_detail_view
from django.core.cache import cache
from django.core.exceptions import BadRequest
from django.db.models import Count, Sum
from django.http import FileResponse, Http404, HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect
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 _
@ -44,7 +47,7 @@ from engine.core.docs.drf.views import (
SEARCH_SCHEMA,
)
from engine.core.elasticsearch import process_query
from engine.core.models import DigitalAssetDownload, Order, OrderProduct
from engine.core.models import DigitalAssetDownload, Order, OrderProduct, Product, Wishlist
from engine.core.serializers import (
BuyAsBusinessOrderSerializer,
CacheOperatorSerializer,
@ -53,6 +56,7 @@ 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
from engine.core.utils.emailing import contact_us_email
from engine.core.utils.languages import get_flag_by_language
from engine.payments.serializers import TransactionProcessSerializer
@ -410,3 +414,86 @@ 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()
quick_links: list[dict[str, str]] = []
with suppress(Exception):
quick_links_section = settings.UNFOLD.get("SIDEBAR", {}).get("navigation", [])[1] # type: ignore[assignment]
for item in quick_links_section.get("items", []):
title = item.get("title")
link = item.get("link")
if not title or not link:
continue
quick_links.append(
{
"title": str(title),
"link": str(link),
**({"icon": item.get("icon")} if item.get("icon") else {}),
}
)
most_wished: dict[str, str | int | float | None] | None = None
with suppress(Exception):
wished = (
Wishlist.objects.filter(user__is_active=True, user__is_staff=False)
.values("products")
.exclude(products__isnull=True)
.annotate(cnt=Count("products"))
.order_by("-cnt")
.first()
)
if wished and wished.get("products"):
product = Product.objects.filter(pk=wished["products"]).first()
if product:
img = product.images.first().image_url if product.images.exists() else ""
most_wished = {
"name": product.name,
"image": img,
"admin_url": reverse("admin:core_product_change", args=[product.pk]),
}
most_popular: dict[str, str | int | float | None] | None = None
with suppress(Exception):
popular = (
OrderProduct.objects.filter(status="FINISHED", order__status="FINISHED", product__isnull=False)
.values("product")
.annotate(total_qty=Sum("quantity"))
.order_by("-total_qty")
.first()
)
if popular and popular.get("product"):
product = Product.objects.filter(pk=popular["product"]).first()
if product:
img = product.images.first().image_url if product.images.exists() else ""
most_popular = {
"name": product.name,
"image": img,
"admin_url": reverse("admin:core_product_change", args=[product.pk]),
}
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,
"evibes_version": settings.EVIBES_VERSION,
"quick_links": quick_links,
"most_wished_product": most_wished,
"most_popular_product": most_popular,
}
)
return context
dashboard_callback.__doc__ = _( # type: ignore [assignment]
"Returns custom variables for Dashboard. "
)

View file

@ -3,9 +3,8 @@ import uuid
from typing import Type
from uuid import UUID
from constance import config
from django.conf import settings
from django.db.models import Prefetch, Q, OuterRef, Exists
from django.db.models import Exists, OuterRef, Prefetch, Q
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
@ -270,7 +269,7 @@ class CategoryViewSet(EvibesViewSet):
def seo_meta(self, request: Request, *args, **kwargs) -> Response:
category = self.get_object()
title = f"{category.name} | {config.PROJECT_NAME}"
title = f"{category.name} | {settings.PROJECT_NAME}"
description = (category.description or "")[:180]
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
og_image = request.build_absolute_uri(category.image.url) if getattr(category, "image", None) else ""
@ -387,7 +386,7 @@ class BrandViewSet(EvibesViewSet):
def seo_meta(self, request: Request, *args, **kwargs) -> Response:
brand = self.get_object()
title = f"{brand.name} | {config.PROJECT_NAME}"
title = f"{brand.name} | {settings.PROJECT_NAME}"
description = (brand.description or "")[:180]
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}"
@ -529,7 +528,7 @@ class ProductViewSet(EvibesViewSet):
p = self.get_object()
images = list(p.images.all()[:6])
rating = {"value": p.rating, "count": p.feedbacks_count}
title = f"{p.name} | {config.PROJECT_NAME}"
title = f"{p.name} | {settings.PROJECT_NAME}"
description = (p.description or "")[:180]
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
og = {

View file

@ -1,15 +1,15 @@
from django.contrib import admin
from django.contrib.admin import ModelAdmin, register
from django.contrib.admin import register
from django.db.models import QuerySet
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from engine.core.admin import ActivationActionsMixin
from engine.payments.forms import GatewayForm, TransactionForm
from engine.payments.models import Balance, Transaction, Gateway
from engine.payments.models import Balance, Gateway, Transaction
class TransactionInline(admin.TabularInline): # type: ignore [type-arg]
class TransactionInline(TabularInline): # type: ignore [type-arg]
model = Transaction
form = TransactionForm
extra = 1

View file

@ -1,9 +1,9 @@
#
#
msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-01-30 03:27+0000\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: BRITISH ENGLISH <CONTACT@FUREUNOIR.COM>\n"
@ -138,6 +138,10 @@ msgstr "لم يتم تعيين مسار تكامل البوابة"
msgid "invalid integration path: %(path)s"
msgstr "مسار تكامل غير صالح: %(path)s"
#: engine/payments/signals.py:41
msgid "the transaction amount didn't fit into allowed limits: "
msgstr "لم يتناسب مبلغ المعاملة مع الحدود المسموح بها:"
#: engine/payments/templates/balance_deposit_email.html:6
#: engine/payments/templates/balance_deposit_email.html:93
msgid "balance deposit"
@ -188,10 +192,36 @@ msgstr "مطلوب مزود للحصول على الأسعار من"
msgid "couldn't find provider {provider}"
msgstr "تعذر العثور على مزود {provider}"
#: engine/payments/utils/emailing.py:27
#: engine/payments/utils/emailing.py:28
#, python-brace-format
msgid "{config.PROJECT_NAME} | balance deposit"
msgstr "{config.PROJECT_NAME} | إيداع الرصيد"
msgid "{settings.PROJECT_NAME} | balance deposit"
msgstr "{settings.PROJECT_NAME} | إيداع الرصيد"
#: engine/payments/views.py:23
msgid ""
"This class provides an API endpoint to handle deposit transactions.\n"
"It supports the creation of a deposit transaction after validating the provided data. If the user is not authenticated, an appropriate response is returned. On successful validation and execution, a response with the transaction details is provided."
msgstr ""
"توفر هذه الفئة نقطة نهاية API للتعامل مع معاملات الإيداع.\n"
"وهي تدعم إنشاء معاملة إيداع بعد التحقق من صحة البيانات المقدمة. إذا لم تتم مصادقة المستخدم، يتم إرجاع استجابة مناسبة. عند التحقق والتنفيذ بنجاح، يتم توفير استجابة بتفاصيل المعاملة."
#: engine/payments/views.py:49
msgid ""
"Handles incoming callback requests to the API.\n"
"This class processes and routes incoming HTTP POST requests to the appropriate pgateway handler based on the provided gateway parameter. It is designed to handle callback events coming from external systems and provide an appropriate HTTP response indicating success or failure."
msgstr ""
"يعالج طلبات رد الاتصال الواردة إلى واجهة برمجة التطبيقات.\n"
"يقوم هذا الصنف بمعالجة طلبات HTTP POST الواردة وتوجيهها إلى معالج pgateway المناسب بناءً على معلمة البوابة المقدمة. وهو مصمم للتعامل مع أحداث رد الاتصال الواردة من أنظمة خارجية وتوفير استجابة HTTP مناسبة تشير إلى النجاح أو الفشل."
#: engine/payments/views.py:60
#, python-brace-format
msgid "Transaction {transaction.uuid} has no gateway"
msgstr "لا تحتوي المعاملة {transaction.uuid} على بوابة"
#: engine/payments/views.py:63
#, python-brace-format
msgid "Gateway {transaction.gateway} has no integration"
msgstr "البوابة {transaction.gateway} ليس لها تكامل"
#: engine/payments/viewsets.py:14
msgid ""
@ -205,4 +235,3 @@ msgstr ""
"للقراءة فقط للتفاعل مع بيانات المعاملات. وتستخدم أداة TransactionSerializer "
"لتسلسل البيانات وإلغاء تسلسلها. تضمن الفئة أن المستخدمين المصرح لهم فقط، "
"الذين يستوفون أذونات محددة، يمكنهم الوصول إلى المعاملات."

View file

@ -1,9 +1,9 @@
#
#
msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2025.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
"PO-Revision-Date: 2025-01-30 03:27+0000\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: BRITISH ENGLISH <CONTACT@FUREUNOIR.COM>\n"
@ -138,6 +138,10 @@ msgstr "cesta integrace brány není nastavena"
msgid "invalid integration path: %(path)s"
msgstr "neplatná cesta integrace: %(path)s"
#: engine/payments/signals.py:41
msgid "the transaction amount didn't fit into allowed limits: "
msgstr "Částka transakce se nevešla do povolených limitů:"
#: engine/payments/templates/balance_deposit_email.html:6
#: engine/payments/templates/balance_deposit_email.html:93
msgid "balance deposit"
@ -188,10 +192,36 @@ msgstr "Je třeba mít poskytovatele, od kterého lze získat sazby"
msgid "couldn't find provider {provider}"
msgstr "Nepodařilo se najít poskytovatele {provider}"
#: engine/payments/utils/emailing.py:27
#: engine/payments/utils/emailing.py:28
#, python-brace-format
msgid "{config.PROJECT_NAME} | balance deposit"
msgstr "{config.PROJECT_NAME} | Zůstatek vkladu"
msgid "{settings.PROJECT_NAME} | balance deposit"
msgstr "{settings.PROJECT_NAME} | zůstatek vkladu"
#: engine/payments/views.py:23
msgid ""
"This class provides an API endpoint to handle deposit transactions.\n"
"It supports the creation of a deposit transaction after validating the provided data. If the user is not authenticated, an appropriate response is returned. On successful validation and execution, a response with the transaction details is provided."
msgstr ""
"Tato třída poskytuje koncový bod API pro zpracování vkladových transakcí.\n"
"Podporuje vytvoření vkladové transakce po ověření zadaných údajů. Pokud uživatel není ověřen, je vrácena odpovídající odpověď. Při úspěšném ověření a provedení je poskytnuta odpověď s údaji o transakci."
#: engine/payments/views.py:49
msgid ""
"Handles incoming callback requests to the API.\n"
"This class processes and routes incoming HTTP POST requests to the appropriate pgateway handler based on the provided gateway parameter. It is designed to handle callback events coming from external systems and provide an appropriate HTTP response indicating success or failure."
msgstr ""
"Zpracovává příchozí požadavky na zpětné volání rozhraní API.\n"
"Tato třída zpracovává a směruje příchozí požadavky HTTP POST na příslušnou obsluhu pgateway na základě zadaného parametru brány. Je navržena tak, aby zpracovávala události zpětného volání přicházející z externích systémů a poskytovala příslušnou odpověď HTTP označující úspěch nebo selhání."
#: engine/payments/views.py:60
#, python-brace-format
msgid "Transaction {transaction.uuid} has no gateway"
msgstr "Transakce {transaction.uuid} nemá žádnou bránu"
#: engine/payments/views.py:63
#, python-brace-format
msgid "Gateway {transaction.gateway} has no integration"
msgstr "Brána {transaction.gateway} nemá žádnou integraci"
#: engine/payments/viewsets.py:14
msgid ""
@ -206,4 +236,3 @@ msgstr ""
"Pro serializaci a deserializaci dat používá TransactionSerializer. Třída "
"zajišťuje, že k transakcím mohou přistupovat pouze oprávnění uživatelé, "
"kteří splňují určitá oprávnění."

Some files were not shown because too many files have changed in this diff Show more