schon/engine/core/admin.py
Egor fureunoir Gorbunov 9cab9fdd3a fix(models): correct discount_price calculation logic
ensure discount_price reflects the actual discount by using the product's price. Also, suppress type-related issues in admin action descriptions for improved linting compatibility.
2026-01-26 14:51:05 +03:00

1189 lines
26 KiB
Python

from contextlib import suppress
from typing import Any, Callable, ClassVar, Type
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 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.typing import FieldsetsType
from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget
from engine.core.forms import (
CRMForm,
OrderForm,
OrderProductForm,
StockForm,
VendorForm,
)
from engine.core.models import (
Address,
Attribute,
AttributeGroup,
AttributeValue,
Brand,
Category,
CategoryTag,
CustomerRelationshipManagementProvider,
Feedback,
Order,
OrderCrmLink,
OrderProduct,
Product,
ProductImage,
ProductTag,
PromoCode,
Promotion,
Stock,
Vendor,
Wishlist,
)
class FieldsetsMixin:
general_fields: list[str] | None = []
relation_fields: list[str] | None = []
additional_fields: list[str] | None = []
model: ClassVar[Type[Model]]
def get_fieldsets(self, request: HttpRequest, obj: Any = None) -> FieldsetsType:
if request:
pass
if obj:
pass
fieldsets = []
def add_translations_fieldset(
fss: FieldsetsType,
) -> FieldsetsType:
with suppress(NotRegistered):
transoptions = translator.get_options_for_model(self.model)
translation_fields = []
for orig in transoptions.local_fields:
translation_fields += get_translation_fields(orig)
if translation_fields:
fss = list(fss) + [
(
_("translations"),
{"classes": ["tab"], "fields": translation_fields},
)
]
return fss
if self.general_fields:
fieldsets.append(
(_("general"), {"classes": ["tab"], "fields": self.general_fields})
)
if self.relation_fields:
fieldsets.append(
(_("relations"), {"classes": ["tab"], "fields": self.relation_fields})
)
if self.additional_fields:
fieldsets.append(
(
_("additional info"),
{"classes": ["tab"], "fields": self.additional_fields},
)
)
opts = self.model._meta
meta_fields = []
if any(f.name == "uuid" for f in opts.fields):
meta_fields.append("uuid")
if any(f.name == "slug" for f in opts.fields):
meta_fields.append("slug")
if any(f.name == "sku" for f in opts.fields):
meta_fields.append("sku")
if any(f.name == "human_readable_id" for f in opts.fields):
meta_fields.append("human_readable_id")
if 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"), {"classes": ["tab"], "fields": ts}))
fieldsets = add_translations_fieldset(fieldsets)
return fieldsets
# noinspection PyUnresolvedReferences
class ActivationActionsMixin:
message_user: Callable
actions_on_top = True
actions_on_bottom = True
actions = [
"delete_selected",
"activate_selected",
"deactivate_selected",
]
@action(
description=_("Activate selected %(verbose_name_plural)s"), # ty:ignore[invalid-argument-type]
permissions=["change"],
)
def activate_selected(self, request: HttpRequest, queryset: QuerySet[Any]) -> None:
try:
queryset.update(is_active=True)
self.message_user(
request=request,
message=_("selected items have been activated.").lower().title(),
level=messages.SUCCESS,
)
except Exception as e:
self.message_user(request=request, message=str(e), level=messages.ERROR)
@action(
description=_("Deactivate selected %(verbose_name_plural)s"), # ty:ignore[invalid-argument-type]
permissions=["change"],
)
def deactivate_selected(
self, request: HttpRequest, queryset: QuerySet[Any]
) -> None:
try:
queryset.update(is_active=False)
self.message_user(
request=request,
message=_("selected items have been deactivated.").lower().title(),
level=messages.SUCCESS,
)
except Exception as e:
self.message_user(request=request, message=str(e), level=messages.ERROR)
class AttributeValueInline(TabularInline):
model = AttributeValue
extra = 0
autocomplete_fields = ["attribute"]
verbose_name = _("attribute value")
verbose_name_plural = _("attribute values")
tab = True
def get_queryset(self, request):
return super().get_queryset(request).select_related("attribute", "product")
class ProductImageInline(TabularInline):
model = ProductImage
extra = 0
tab = True
verbose_name = _("image")
verbose_name_plural = _("images")
def get_queryset(self, request):
return super().get_queryset(request).select_related("product")
class StockInline(TabularInline):
model = Stock
extra = 0
form = StockForm
tab = True
verbose_name = _("stock")
verbose_name_plural = _("stocks")
def get_queryset(self, request):
return super().get_queryset(request).select_related("vendor", "product")
class OrderProductInline(TabularInline):
model = OrderProduct
extra = 0
readonly_fields = ("product", "quantity", "buy_price")
form = OrderProductForm
verbose_name = _("order product")
verbose_name_plural = _("order products")
tab = True
def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related("product")
.only("product__name")
)
class CategoryChildrenInline(TabularInline):
model = Category
fk_name = "parent"
extra = 0
fields = ("name", "description", "is_active", "image", "markup_percent")
tab = True
verbose_name = _("children")
verbose_name_plural = _("children")
@register(AttributeGroup)
class AttributeGroupAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = AttributeGroup
list_display = (
"name",
"modified",
)
search_fields = (
"uuid",
"name",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
general_fields = [
"is_active",
"name",
"parent",
]
@register(Attribute)
class AttributeAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Attribute
list_display = (
"name",
"group",
"value_type",
"modified",
)
list_filter = (
"value_type",
"group",
"is_active",
)
search_fields = (
"uuid",
"name",
"group__name",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
autocomplete_fields = [
"group",
]
general_fields = [
"is_active",
"name",
"value_type",
"is_filterable",
]
relation_fields = [
"group",
]
@register(AttributeValue)
class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
# noinspection PyClassVar
model = AttributeValue
list_display = (
"attribute",
"value",
"modified",
)
list_filter = (
"attribute__group",
"is_active",
)
search_fields = (
"uuid",
"value",
"attribute__name",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
autocomplete_fields = [
"attribute",
]
general_fields = [
"is_active",
"value",
]
relation_fields = [
"attribute",
"product",
]
@register(Category)
class CategoryAdmin(
DjangoQLSearchMixin,
FieldsetsMixin,
ActivationActionsMixin,
DraggableMPTTAdmin,
ModelAdmin,
):
# noinspection PyClassVar
model = Category
list_display = (
"indented_title",
"parent",
"is_active",
"modified",
)
# noinspection PyUnresolvedReferences
list_filter = (
"is_active",
"level",
"created",
"modified",
)
search_fields = (
"uuid",
"name",
)
inlines = [
CategoryChildrenInline,
]
autocomplete_fields = [
"parent",
"tags",
]
readonly_fields = (
"slug",
"uuid",
"modified",
"created",
)
general_fields = [
"is_active",
"name",
"description",
"image",
"markup_percent",
"priority",
]
relation_fields = [
"parent",
"tags",
]
@register(Brand)
class BrandAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Brand
list_display = (
"name",
"priority",
"is_active",
)
list_filter = (
"categories",
"is_active",
)
search_fields = (
"uuid",
"name",
)
readonly_fields = (
"uuid",
"slug",
"modified",
"created",
)
general_fields = [
"is_active",
"name",
"description",
"priority",
]
additional_fields = ["small_logo", "big_logo"]
@register(Product)
class ProductAdmin(
DjangoQLSearchMixin,
FieldsetsMixin,
ActivationActionsMixin,
ModelAdmin,
ImportExportModelAdmin,
):
# noinspection PyClassVar
model = Product
list_display = (
"sku",
"name",
"is_active",
"export_to_marketplaces",
"has_images",
"category",
"brand",
"price",
"rating",
"modified",
)
list_filter = (
"is_active",
"is_digital",
"is_updatable",
"stocks__vendor",
"tags__name",
"created",
"modified",
)
search_fields = (
"name",
"partnumber",
"brand__name",
"brand__slug",
"category__name",
"category__slug",
"uuid",
"slug",
"sku",
)
readonly_fields = (
"sku",
"slug",
"uuid",
"modified",
"created",
)
autocomplete_fields = (
"category",
"brand",
"tags",
)
inlines = [
AttributeValueInline,
ProductImageInline,
StockInline,
]
import_form_class = ImportForm
export_form_class = ExportForm
general_fields = [
"is_active",
"name",
"partnumber",
"is_digital",
]
relation_fields = [
"category",
"brand",
"tags",
]
additional_fields = [
"is_updatable",
]
def has_images(self, obj: Product) -> bool:
return obj.has_images
has_images.boolean = True # ty:ignore[unresolved-attribute]
has_images.short_description = _("has images") # ty:ignore[unresolved-attribute]
@action(
description=_("Export selected %(verbose_name_plural)s to marketplaces' feeds"), # ty:ignore[invalid-argument-type]
permissions=["change"],
)
def export_to_marketplaces(
self, request: HttpRequest, queryset: QuerySet[Any]
) -> None:
try:
queryset.update(export_to_marketplaces=True)
self.message_user(
request=request,
message=_(
"selected %(verbose_name_plural)s have been marked for export."
)
.lower()
.title(),
level=messages.SUCCESS,
)
except Exception as e:
self.message_user(request=request, message=str(e), level=messages.ERROR)
@action(
description=_("Ban selected %(verbose_name_plural)s from marketplaces' feeds"), # ty:ignore[invalid-argument-type]
permissions=["change"],
)
def ban_from_marketplaces(
self, request: HttpRequest, queryset: QuerySet[Any]
) -> None:
try:
queryset.update(export_to_marketplaces=False)
self.message_user(
request=request,
message=_(
"selected %(verbose_name_plural)s have been banned from export."
)
.lower()
.title(),
level=messages.SUCCESS,
)
except Exception as e:
self.message_user(request=request, message=str(e), level=messages.ERROR)
def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related("category", "brand")
.prefetch_related(
"tags",
"images",
"stocks__vendor",
"attributes__attribute",
)
)
@register(ProductTag)
class ProductTagAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = ProductTag
list_display = ("tag_name",)
search_fields = ("tag_name",)
readonly_fields = (
"uuid",
"modified",
"created",
)
general_fields = [
"is_active",
"tag_name",
"name",
]
@register(CategoryTag)
class CategoryTagAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = CategoryTag
list_display = (
"name",
"tag_name",
"is_active",
)
search_fields = (
"name",
"tag_name",
"is_active",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
general_fields = [
"is_active",
"tag_name",
"name",
]
@register(Vendor)
class VendorAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Vendor
list_display = (
"name",
"markup_percent",
"modified",
)
list_filter = (
"markup_percent",
"is_active",
)
search_fields = (
"name",
"uuid",
)
readonly_fields = (
"uuid",
"modified",
"created",
"last_processing_response",
)
form = VendorForm
general_fields = [
"is_active",
"name",
"markup_percent",
"authentication",
]
relation_fields = [
"users",
]
additional_fields = [
"integration_path",
"last_processing_response",
"b2b_auth_token",
]
@register(Feedback)
class FeedbackAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Feedback
list_display = (
"order_product",
"rating",
"comment",
"modified",
)
list_filter = (
"rating",
"is_active",
)
search_fields = (
"order_product__product__name",
"comment",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
general_fields = [
"is_active",
"rating",
"comment",
]
relation_fields = [
"order_product",
]
@register(Order)
class OrderAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Order
list_display = (
"human_readable_id",
"user",
"status",
"total_price",
"buy_time",
"modified",
)
list_filter = (
"status",
"buy_time",
"modified",
"created",
)
search_fields = (
"user__email",
"status",
"uuid",
"human_readable_id",
)
readonly_fields = (
"total_price",
"total_quantity",
"human_readable_id",
"uuid",
"modified",
"created",
)
inlines = [
OrderProductInline,
]
form = OrderForm
general_fields = [
"is_active",
"user",
"status",
"notifications",
"attributes",
"buy_time",
]
relation_fields = [
"promo_code",
"billing_address",
"shipping_address",
]
@register(OrderProduct)
class OrderProductAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = OrderProduct
list_display = (
"order",
"product",
"quantity",
"buy_price",
"status",
"modified",
)
list_filter = (
"status",
"modified",
)
search_fields = (
"order__user__email",
"product__name",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
form = OrderProductForm
general_fields = [
"is_active",
"quantity",
"buy_price",
"status",
]
relation_fields = [
"order",
"product",
]
@register(PromoCode)
class PromoCodeAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = PromoCode
list_display = (
"code",
"discount_percent",
"discount_amount",
"start_time",
"end_time",
"used_on",
)
list_filter = (
"discount_percent",
"discount_amount",
"start_time",
"end_time",
)
search_fields = (
"code",
"uuid",
"user__email",
)
readonly_fields = (
"used_on",
"uuid",
"modified",
"created",
)
autocomplete_fields = ("user",)
general_fields = [
"is_active",
"code",
"discount_amount",
"discount_percent",
"start_time",
"end_time",
"used_on",
]
relation_fields = [
"user",
]
@register(Promotion)
class PromotionAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Promotion
list_display = (
"name",
"discount_percent",
"modified",
)
search_fields = ("name",)
readonly_fields = (
"uuid",
"modified",
"created",
)
autocomplete_fields = ("products",)
general_fields = [
"is_active",
"name",
"discount_percent",
"description",
]
relation_fields = [
"products",
]
@register(Stock)
class StockAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Stock
form = StockForm
list_display = (
"product",
"vendor",
"sku",
"quantity",
"price",
"modified",
)
list_filter = (
"vendor",
"quantity",
)
search_fields = (
"product__name",
"vendor__name",
"sku",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
autocomplete_fields = (
"product",
"vendor",
)
general_fields = [
"is_active",
"sku",
"quantity",
"price",
"purchase_price",
"digital_asset",
]
additional_fields = [
"system_attributes",
]
relation_fields = [
"product",
"vendor",
]
@register(Wishlist)
class WishlistAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = Wishlist
list_display = (
"user",
"modified",
)
search_fields = (
"user__email",
"uuid",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
general_fields = [
"is_active",
"user",
]
relation_fields = [
"products",
]
@register(ProductImage)
class ProductImageAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
):
# noinspection PyClassVar
model = ProductImage
list_display = (
"alt",
"product",
"priority",
"modified",
)
list_filter = (
"priority",
"modified",
"created",
)
search_fields = (
"alt",
"product__name",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
autocomplete_fields = ("product",)
general_fields = [
"is_active",
"alt",
"priority",
"image",
]
relation_fields = [
"product",
]
@register(Address)
class AddressAdmin(DjangoQLSearchMixin, FieldsetsMixin, GISModelAdmin):
# noinspection PyClassVar
model = Address
list_display = (
"street",
"city",
"region",
"country",
"user",
)
list_filter = (
"country",
"region",
)
search_fields = (
"street",
"city",
"postal_code",
"user__email",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
gis_widget_kwargs = {
"attrs": {
"default_lon": 37.61556,
"default_lat": 55.75222,
"default_zoom": 6,
}
}
general_fields = [
"is_active",
"address_line",
"street",
"district",
"city",
"region",
"postal_code",
"country",
"raw_data",
]
relation_fields = [
"user",
"api_response",
]
@register(CustomerRelationshipManagementProvider)
class CustomerRelationshipManagementProviderAdmin(
DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin
):
# noinspection PyClassVar
model = CustomerRelationshipManagementProvider
list_display = (
"name",
"default",
)
search_fields = (
"name",
"uuid",
)
readonly_fields = (
"uuid",
"modified",
"created",
)
form = CRMForm
general_fields = [
"is_active",
"name",
"default",
"integration_url",
"integration_location",
"attributes",
"authentication",
]
@register(OrderCrmLink)
class OrderCrmLinkAdmin(DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin):
# noinspection PyClassVar
model = OrderCrmLink
list_display = (
"crm_lead_id",
"order",
)
search_fields = (
"crm_lead_id",
"order__human_readable_id",
"order__uuid",
"order__user__uuid",
"order__user__email",
)
readonly_fields = (
"uuid",
"modified",
"created",
"crm_lead_id",
)
general_fields = [
"is_active",
"crm_lead_id",
]
relation_fields = [
"order",
"crm",
]
# Constance configuration
class ConstanceConfig:
class Meta:
app_label = "core"
object_name = "Config"
concrete_model = None
model_name = module_name = "config"
verbose_name_plural = _("Config")
abstract = False
swapped = False
is_composite_pk = False
def get_change_permission(self) -> str:
return f"change_{self.model_name}"
@property
def app_config(self) -> AppConfig:
return apps.get_app_config(self.app_label)
@property
def label(self) -> str:
return f"{self.app_label}.{self.object_name}"
@property
def label_lower(self) -> str:
return f"{self.app_label}.{self.model_name}"
def get_ordered_objects(self) -> bool:
return False
_meta = Meta()
site.unregister([Config]) # ty:ignore[invalid-argument-type]
site.register([ConstanceConfig], BaseConstanceAdmin) # ty:ignore[invalid-argument-type]
site.site_title = settings.PROJECT_NAME
site.site_header = "Schon"
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