311 lines
12 KiB
Python
311 lines
12 KiB
Python
from django.contrib import admin
|
||
from django.utils.html import format_html
|
||
import nested_admin
|
||
from .models import ProductCategory, ProductTag, Product, ProductKit, KitItem
|
||
from .models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
|
||
from .models import ProductVariantGroup, KitItemPriority, SKUCounter
|
||
|
||
|
||
@admin.register(ProductVariantGroup)
|
||
class ProductVariantGroupAdmin(admin.ModelAdmin):
|
||
list_display = ['name', 'get_products_count', 'created_at']
|
||
search_fields = ['name', 'description']
|
||
list_filter = ['created_at']
|
||
readonly_fields = ['created_at', 'updated_at']
|
||
|
||
def get_products_count(self, obj):
|
||
return obj.products.count()
|
||
get_products_count.short_description = 'Товаров'
|
||
|
||
|
||
class ProductCategoryAdmin(admin.ModelAdmin):
|
||
list_display = ('photo_preview', 'name', 'sku', 'slug', 'parent', 'is_active')
|
||
list_filter = ('is_active', 'parent')
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
search_fields = ('name', 'sku')
|
||
readonly_fields = ('photo_preview_large',)
|
||
|
||
def photo_preview(self, obj):
|
||
"""Превью фото в списке категорий"""
|
||
first_photo = obj.photos.first()
|
||
if first_photo and first_photo.image:
|
||
return format_html(
|
||
'<img src="{}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;" />',
|
||
first_photo.image.url
|
||
)
|
||
return "Нет фото"
|
||
photo_preview.short_description = "Фото"
|
||
|
||
def photo_preview_large(self, obj):
|
||
"""Большое превью фото в форме редактирования"""
|
||
first_photo = obj.photos.first()
|
||
if first_photo and first_photo.image:
|
||
return format_html(
|
||
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 8px;" />',
|
||
first_photo.image.url
|
||
)
|
||
return "Нет фото"
|
||
photo_preview_large.short_description = "Превью основного фото"
|
||
|
||
|
||
class ProductTagAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
search_fields = ('name',)
|
||
|
||
|
||
class ProductAdmin(admin.ModelAdmin):
|
||
list_display = ('photo_preview', 'name', 'sku', 'get_categories_display', 'cost_price', 'sale_price', 'get_variant_groups_display', 'is_active')
|
||
list_filter = ('is_active', 'categories', 'tags', 'variant_groups')
|
||
search_fields = ('name', 'sku', 'description', 'search_keywords')
|
||
filter_horizontal = ('categories', 'tags', 'variant_groups')
|
||
readonly_fields = ('photo_preview_large',)
|
||
autocomplete_fields = []
|
||
|
||
fieldsets = (
|
||
('Основная информация', {
|
||
'fields': ('name', 'sku', 'variant_suffix', 'description', 'categories', 'unit')
|
||
}),
|
||
('Цены', {
|
||
'fields': ('cost_price', 'sale_price')
|
||
}),
|
||
('Дополнительно', {
|
||
'fields': ('tags', 'variant_groups', 'is_active')
|
||
}),
|
||
('Поиск', {
|
||
'fields': ('search_keywords',),
|
||
'classes': ('collapse',),
|
||
'description': 'Поле для улучшенного поиска. Автоматически генерируется при сохранении, но вы можете добавить дополнительные ключевые слова (синонимы, альтернативные названия и т.д.).'
|
||
}),
|
||
('Фото', {
|
||
'fields': ('photo_preview_large',),
|
||
'classes': ('collapse',),
|
||
}),
|
||
)
|
||
|
||
def get_categories_display(self, obj):
|
||
categories = obj.categories.all()[:3]
|
||
if not categories:
|
||
return "-"
|
||
result = ", ".join([cat.name for cat in categories])
|
||
if obj.categories.count() > 3:
|
||
result += f" (+{obj.categories.count() - 3})"
|
||
return result
|
||
get_categories_display.short_description = 'Категории'
|
||
|
||
def get_variant_groups_display(self, obj):
|
||
groups = obj.variant_groups.all()[:3]
|
||
if not groups:
|
||
return "-"
|
||
result = ", ".join([g.name for g in groups])
|
||
if obj.variant_groups.count() > 3:
|
||
result += f" (+{obj.variant_groups.count() - 3})"
|
||
return result
|
||
get_variant_groups_display.short_description = 'Группы вариантов'
|
||
|
||
def photo_preview(self, obj):
|
||
"""Превью фото в списке товаров"""
|
||
first_photo = obj.photos.first()
|
||
if first_photo and first_photo.image:
|
||
return format_html(
|
||
'<img src="{}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;" />',
|
||
first_photo.image.url
|
||
)
|
||
return "Нет фото"
|
||
photo_preview.short_description = "Фото"
|
||
|
||
def photo_preview_large(self, obj):
|
||
"""Большое превью фото в форме редактирования"""
|
||
first_photo = obj.photos.first()
|
||
if first_photo and first_photo.image:
|
||
return format_html(
|
||
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 8px;" />',
|
||
first_photo.image.url
|
||
)
|
||
return "Нет фото"
|
||
photo_preview_large.short_description = "Превью основного фото"
|
||
|
||
|
||
class ProductKitAdmin(admin.ModelAdmin):
|
||
list_display = ('photo_preview', 'name', 'slug', 'get_categories_display', 'pricing_method', 'is_active')
|
||
list_filter = ('is_active', 'pricing_method', 'categories', 'tags')
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
filter_horizontal = ('categories', 'tags')
|
||
readonly_fields = ('photo_preview_large',)
|
||
|
||
def get_categories_display(self, obj):
|
||
categories = obj.categories.all()[:3]
|
||
if not categories:
|
||
return "-"
|
||
result = ", ".join([cat.name for cat in categories])
|
||
if obj.categories.count() > 3:
|
||
result += f" (+{obj.categories.count() - 3})"
|
||
return result
|
||
get_categories_display.short_description = 'Категории'
|
||
|
||
def photo_preview(self, obj):
|
||
"""Превью фото в списке комплектов"""
|
||
first_photo = obj.photos.first()
|
||
if first_photo and first_photo.image:
|
||
return format_html(
|
||
'<img src="{}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;" />',
|
||
first_photo.image.url
|
||
)
|
||
return "Нет фото"
|
||
photo_preview.short_description = "Фото"
|
||
|
||
def photo_preview_large(self, obj):
|
||
"""Большое превью фото в форме редактирования"""
|
||
first_photo = obj.photos.first()
|
||
if first_photo and first_photo.image:
|
||
return format_html(
|
||
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 8px;" />',
|
||
first_photo.image.url
|
||
)
|
||
return "Нет фото"
|
||
photo_preview_large.short_description = "Превью основного фото"
|
||
|
||
|
||
class KitItemPriorityInline(nested_admin.NestedTabularInline):
|
||
model = KitItemPriority
|
||
extra = 0 # Не показывать пустые формы
|
||
fields = ['product', 'priority']
|
||
autocomplete_fields = ['product']
|
||
|
||
def get_queryset(self, request):
|
||
qs = super().get_queryset(request)
|
||
return qs.select_related('product')
|
||
|
||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||
"""Показывать только товары из выбранной группы вариантов"""
|
||
if db_field.name == "product":
|
||
# Получаем kit_item из родительского объекта через request
|
||
# Это будет работать автоматически с nested_admin
|
||
pass
|
||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||
|
||
|
||
class KitItemInline(nested_admin.NestedStackedInline):
|
||
model = KitItem
|
||
extra = 0 # Не показывать пустые формы
|
||
fields = ['product', 'variant_group', 'quantity', 'notes']
|
||
autocomplete_fields = ['product']
|
||
inlines = [KitItemPriorityInline]
|
||
|
||
class Media:
|
||
css = {
|
||
'all': ('admin/css/custom_nested.css',)
|
||
}
|
||
|
||
|
||
class ProductPhotoInline(admin.TabularInline):
|
||
model = ProductPhoto
|
||
extra = 1
|
||
readonly_fields = ('image_preview',)
|
||
fields = ('image', 'image_preview', 'order')
|
||
|
||
def image_preview(self, obj):
|
||
"""Превью загруженного фото"""
|
||
if obj.image:
|
||
return format_html(
|
||
'<img src="{}" style="max-width: 200px; max-height: 200px; border-radius: 4px;" />',
|
||
obj.image.url
|
||
)
|
||
return "Нет изображения"
|
||
image_preview.short_description = "Превью"
|
||
|
||
class ProductKitPhotoInline(nested_admin.NestedTabularInline):
|
||
model = ProductKitPhoto
|
||
extra = 0 # Не показывать пустые формы
|
||
readonly_fields = ('image_preview',)
|
||
fields = ('image', 'image_preview', 'order')
|
||
|
||
def image_preview(self, obj):
|
||
"""Превью загруженного фото"""
|
||
if obj.image:
|
||
return format_html(
|
||
'<img src="{}" style="max-width: 200px; max-height: 200px; border-radius: 4px;" />',
|
||
obj.image.url
|
||
)
|
||
return "Нет изображения"
|
||
image_preview.short_description = "Превью"
|
||
|
||
class ProductCategoryPhotoInline(admin.TabularInline):
|
||
model = ProductCategoryPhoto
|
||
extra = 1
|
||
readonly_fields = ('image_preview',)
|
||
fields = ('image', 'image_preview', 'order')
|
||
|
||
def image_preview(self, obj):
|
||
"""Превью загруженного фото"""
|
||
if obj.image:
|
||
return format_html(
|
||
'<img src="{}" style="max-width: 200px; max-height: 200px; border-radius: 4px;" />',
|
||
obj.image.url
|
||
)
|
||
return "Нет изображения"
|
||
image_preview.short_description = "Превью"
|
||
|
||
class ProductKitAdminWithItems(ProductKitAdmin):
|
||
inlines = [KitItemInline]
|
||
|
||
|
||
# Update admin classes to include photo inlines
|
||
class ProductAdminWithPhotos(ProductAdmin):
|
||
inlines = [ProductPhotoInline]
|
||
|
||
class ProductKitAdminWithItemsAndPhotos(nested_admin.NestedModelAdmin, ProductKitAdmin):
|
||
inlines = [KitItemInline, ProductKitPhotoInline]
|
||
|
||
class ProductCategoryAdminWithPhotos(ProductCategoryAdmin):
|
||
inlines = [ProductCategoryPhotoInline]
|
||
|
||
|
||
@admin.register(KitItem)
|
||
class KitItemAdmin(admin.ModelAdmin):
|
||
list_display = ['__str__', 'kit', 'get_type', 'quantity', 'has_priorities']
|
||
list_filter = ['kit']
|
||
list_select_related = ['kit', 'product', 'variant_group']
|
||
inlines = [KitItemPriorityInline]
|
||
fields = ['kit', 'product', 'variant_group', 'quantity', 'notes']
|
||
|
||
def get_type(self, obj):
|
||
if obj.variant_group:
|
||
return format_html('<span style="color: #0066cc;">Группа: {}</span>', obj.variant_group.name)
|
||
return f"Товар: {obj.product.name if obj.product else '-'}"
|
||
get_type.short_description = 'Тип'
|
||
|
||
def has_priorities(self, obj):
|
||
return obj.priorities.exists()
|
||
has_priorities.boolean = True
|
||
has_priorities.short_description = 'Приоритеты настроены'
|
||
|
||
|
||
@admin.register(SKUCounter)
|
||
class SKUCounterAdmin(admin.ModelAdmin):
|
||
list_display = ['counter_type', 'current_value', 'get_next_preview']
|
||
list_filter = ['counter_type']
|
||
readonly_fields = ['get_next_preview']
|
||
|
||
def get_next_preview(self, obj):
|
||
"""Показывает, каким будет следующий артикул"""
|
||
next_val = obj.current_value + 1
|
||
if obj.counter_type == 'product':
|
||
return format_html('<strong>PROD-{:06d}</strong>', next_val)
|
||
elif obj.counter_type == 'kit':
|
||
return format_html('<strong>KIT-{:06d}</strong>', next_val)
|
||
elif obj.counter_type == 'category':
|
||
return format_html('<strong>CAT-{:04d}</strong>', next_val)
|
||
return str(next_val)
|
||
get_next_preview.short_description = 'Следующий артикул'
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
# Запрещаем удаление счетчиков
|
||
return False
|
||
|
||
|
||
admin.site.register(ProductCategory, ProductCategoryAdminWithPhotos)
|
||
admin.site.register(ProductTag, ProductTagAdmin)
|
||
admin.site.register(Product, ProductAdminWithPhotos)
|
||
admin.site.register(ProductKit, ProductKitAdminWithItemsAndPhotos)
|