Скрыты временные комплекты из каталога и поиска

Views:
- ProductKitListView: фильтр is_temporary=False
- CombinedProductListView: фильтр is_temporary=False для комплектов
- API search: фильтр is_temporary=False в поиске и популярных

Admin:
- Добавлен фильтр по is_temporary
- Добавлено отображение статуса временного комплекта в списке
- Добавлена ссылка на заказ для временных комплектов
- Добавлен раздел "Временный комплект" в fieldsets

Теперь временные комплекты не показываются в общем каталоге,
но доступны в админке и по прямой ссылке (для заказов).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 15:00:25 +03:00
parent 7b78ad2a6e
commit ea4bb5a43b
4 changed files with 32 additions and 11 deletions

View File

@@ -508,11 +508,11 @@ class ProductAdmin(admin.ModelAdmin):
class ProductKitAdmin(admin.ModelAdmin): class ProductKitAdmin(admin.ModelAdmin):
list_display = ('photo_with_quality', 'name', 'slug', 'get_categories_display', 'get_price_display', 'is_active', 'get_deleted_status') list_display = ('photo_with_quality', 'name', 'slug', 'get_categories_display', 'get_price_display', 'is_temporary', 'get_order_link', 'is_active', 'get_deleted_status')
list_filter = (DeletedFilter, 'is_active', QualityLevelFilter, 'categories', 'tags') list_filter = (DeletedFilter, 'is_active', 'is_temporary', QualityLevelFilter, 'categories', 'tags')
prepopulated_fields = {'slug': ('name',)} prepopulated_fields = {'slug': ('name',)}
filter_horizontal = ('categories', 'tags') filter_horizontal = ('categories', 'tags')
readonly_fields = ('photo_preview_large', 'base_price', 'deleted_at', 'deleted_by') readonly_fields = ('photo_preview_large', 'base_price', 'deleted_at', 'deleted_by', 'order')
actions = [ actions = [
restore_items, restore_items,
delete_selected, delete_selected,
@@ -530,6 +530,10 @@ class ProductKitAdmin(admin.ModelAdmin):
'fields': ('base_price', 'price', 'sale_price', 'price_adjustment_type', 'price_adjustment_value'), 'fields': ('base_price', 'price', 'sale_price', 'price_adjustment_type', 'price_adjustment_value'),
'description': 'base_price - сумма цен компонентов (вычисляется автоматически). price - итоговая цена с учетом корректировок. sale_price - цена со скидкой (опционально).' 'description': 'base_price - сумма цен компонентов (вычисляется автоматически). price - итоговая цена с учетом корректировок. sale_price - цена со скидкой (опционально).'
}), }),
('Временный комплект', {
'fields': ('is_temporary', 'order'),
'description': 'Временные комплекты создаются для конкретных заказов и не показываются в каталоге.'
}),
('Дополнительно', { ('Дополнительно', {
'fields': ('tags', 'is_active') 'fields': ('tags', 'is_active')
}), }),
@@ -552,6 +556,15 @@ class ProductKitAdmin(admin.ModelAdmin):
return "-" return "-"
get_price_display.short_description = "Цена" get_price_display.short_description = "Цена"
def get_order_link(self, obj):
"""Отображение ссылки на заказ для временных комплектов"""
if obj.order:
from django.urls import reverse
url = reverse('admin:orders_order_change', args=[obj.order.pk])
return format_html('<a href="{}">{}</a>', url, obj.order.order_number)
return '-'
get_order_link.short_description = "Заказ"
def get_queryset(self, request): def get_queryset(self, request):
"""Переопределяем queryset для доступа ко всем комплектам (включая удаленные)""" """Переопределяем queryset для доступа ко всем комплектам (включая удаленные)"""
qs = ProductKit.all_objects.all() qs = ProductKit.all_objects.all()

View File

@@ -83,6 +83,8 @@ def search_products_and_variants(request):
'pagination': {'more': False} 'pagination': {'more': False}
}) })
elif item_type == 'kit': elif item_type == 'kit':
# Для комплектов: временные комплекты можно получать по ID (для заказов)
# но не показываем их в общем поиске
kit = ProductKit.objects.get(id=numeric_id, is_active=True) kit = ProductKit.objects.get(id=numeric_id, is_active=True)
return JsonResponse({ return JsonResponse({
'results': [{ 'results': [{
@@ -91,7 +93,8 @@ def search_products_and_variants(request):
'sku': kit.sku, 'sku': kit.sku,
'price': str(kit.price) if kit.price else None, 'price': str(kit.price) if kit.price else None,
'actual_price': str(kit.actual_price) if kit.actual_price else '0', 'actual_price': str(kit.actual_price) if kit.actual_price else '0',
'type': 'kit' 'type': 'kit',
'is_temporary': kit.is_temporary
}], }],
'pagination': {'more': False} 'pagination': {'more': False}
}) })
@@ -142,8 +145,8 @@ def search_products_and_variants(request):
}) })
if search_type in ['all', 'kit']: if search_type in ['all', 'kit']:
# Показываем последние добавленные активные комплекты # Показываем последние добавленные активные комплекты (только постоянные)
kits = ProductKit.objects.filter(is_active=True)\ kits = ProductKit.objects.filter(is_active=True, is_temporary=False)\
.order_by('-created_at')[:page_size]\ .order_by('-created_at')[:page_size]\
.values('id', 'name', 'sku', 'price', 'sale_price') .values('id', 'name', 'sku', 'price', 'sale_price')
@@ -270,7 +273,7 @@ def search_products_and_variants(request):
# Поиск комплектов # Поиск комплектов
if search_type in ['all', 'kit']: if search_type in ['all', 'kit']:
# Используем аналогичную логику для комплектов # Используем аналогичную логику для комплектов (только постоянные)
if 'sqlite' in settings.DATABASES['default']['ENGINE']: if 'sqlite' in settings.DATABASES['default']['ENGINE']:
from django.db.models.functions import Lower from django.db.models.functions import Lower
query_lower = query_normalized.lower() query_lower = query_normalized.lower()
@@ -283,7 +286,8 @@ def search_products_and_variants(request):
models.Q(name_lower__contains=query_lower) | models.Q(name_lower__contains=query_lower) |
models.Q(sku_lower__contains=query_lower) | models.Q(sku_lower__contains=query_lower) |
models.Q(description_lower__contains=query_lower), models.Q(description_lower__contains=query_lower),
is_active=True is_active=True,
is_temporary=False
).annotate( ).annotate(
relevance=Case( relevance=Case(
When(name_lower=query_lower, then=3), When(name_lower=query_lower, then=3),
@@ -297,7 +301,8 @@ def search_products_and_variants(request):
models.Q(name__icontains=query_normalized) | models.Q(name__icontains=query_normalized) |
models.Q(sku__icontains=query_normalized) | models.Q(sku__icontains=query_normalized) |
models.Q(description__icontains=query_normalized), models.Q(description__icontains=query_normalized),
is_active=True is_active=True,
is_temporary=False
).annotate( ).annotate(
relevance=Case( relevance=Case(
When(name__iexact=query_normalized, then=3), When(name__iexact=query_normalized, then=3),

View File

@@ -194,9 +194,9 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV
paginate_by = 20 paginate_by = 20
def get_queryset(self): def get_queryset(self):
# Получаем товары и комплекты # Получаем товары и комплекты (только постоянные комплекты)
products = Product.objects.prefetch_related('categories', 'photos', 'tags') products = Product.objects.prefetch_related('categories', 'photos', 'tags')
kits = ProductKit.objects.prefetch_related('categories', 'photos') kits = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos')
# Применяем фильтры # Применяем фильтры
search_query = self.request.GET.get('search') search_query = self.request.GET.get('search')

View File

@@ -24,6 +24,9 @@ class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset.prefetch_related('categories', 'photos', 'kit_items', 'tags') queryset = queryset.prefetch_related('categories', 'photos', 'kit_items', 'tags')
# Скрываем временные комплекты из общего каталога
queryset = queryset.filter(is_temporary=False)
# Поиск по названию # Поиск по названию
search_query = self.request.GET.get('search') search_query = self.request.GET.get('search')
if search_query: if search_query: