Объединение списков товаров и комплектов в единый интерфейс

- Создан единый шаблон products_list.html для отображения товаров и комплектов
- Удалены дублирующиеся шаблоны (product_list, productkit_list, products_unified_list, all_products_list)
- Добавлены фильтры: тип (все/товары/комплекты), категория, статус, наличие, теги
- Обновлен CombinedProductListView с поддержкой фильтрации по типу и тегам
- Изменены URL маршруты: главная страница /products/ теперь показывает объединенный список
- Обновлены success_url во всех CRUD представлениях для редиректа на объединенный список
- Добавлена фильтрация по тегам с отображением количества выбранных элементов
- Улучшена UX: компактный select для тегов с счетчиком выбранных
- Все комментарии в коде переведены на русский язык
This commit is contained in:
2025-11-15 22:48:34 +03:00
parent 9363527e50
commit b8185f2f6c
8 changed files with 409 additions and 768 deletions

View File

@@ -115,7 +115,7 @@ class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
permission_required = 'products.add_product'
def get_success_url(self):
return reverse_lazy('products:product-list')
return reverse_lazy('products:products-list')
def form_valid(self, form):
from django.db import IntegrityError
@@ -123,7 +123,7 @@ class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
try:
response = super().form_valid(form)
# Handle photo uploads
# Обработка загрузки фотографий
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
if photo_errors:
for error in photo_errors:
@@ -162,7 +162,7 @@ class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView)
permission_required = 'products.view_product'
def get_queryset(self):
# Prefetch photos to avoid N+1 queries
# Предзагрузка фотографий для избежания N+1 запросов
return super().get_queryset().prefetch_related('photos')
def get_context_data(self, **kwargs):
@@ -180,7 +180,7 @@ class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView)
permission_required = 'products.change_product'
def get_success_url(self):
return reverse_lazy('products:product-list')
return reverse_lazy('products:products-list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -195,7 +195,7 @@ class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView)
try:
response = super().form_valid(form)
# Handle photo uploads
# Обработка загрузки фотографий
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
if photo_errors:
for error in photo_errors:
@@ -235,29 +235,34 @@ class ProductDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView)
def get_success_url(self):
messages.success(self.request, f'Товар "{self.object.name}" успешно удален!')
return reverse_lazy('products:product-list')
return reverse_lazy('products:products-list')
class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Объединенное представление для товаров и комплектов.
Показывает оба типа продуктов в одном списке.
Показывает оба типа продуктов в одном списке с возможностью фильтрации по типу.
"""
template_name = 'products/all_products_list.html'
template_name = 'products/products_list.html'
context_object_name = 'items'
permission_required = 'products.view_product'
paginate_by = 20
def get_queryset(self):
# Получаем фильтр по типу
type_filter = self.request.GET.get('type', 'all')
# Получаем товары и комплекты (только постоянные комплекты)
products = Product.objects.prefetch_related('categories', 'photos', 'tags')
kits = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos')
kits = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos', 'tags')
# Применяем фильтры
search_query = self.request.GET.get('search')
category_id = self.request.GET.get('category')
status_filter = self.request.GET.get('status')
is_active_filter = self.request.GET.get('is_active')
in_stock_filter = self.request.GET.get('in_stock')
tags = self.request.GET.getlist('tags')
# Фильтрация по поиску
if search_query:
@@ -293,15 +298,31 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV
elif is_active_filter == '0':
products = products.filter(status__in=['archived', 'discontinued'])
kits = kits.filter(status__in=['archived', 'discontinued'])
# Фильтрация по наличию (только для товаров)
if in_stock_filter == '1':
products = products.filter(in_stock=True)
elif in_stock_filter == '0':
products = products.filter(in_stock=False)
# Фильтрация по тегам
if tags:
products = products.filter(tags__id__in=tags).distinct()
kits = kits.filter(tags__id__in=tags).distinct()
# Добавляем type для различения в шаблоне
products_list = list(products.order_by('-created_at'))
for p in products_list:
p.item_type = 'product'
kits_list = list(kits.order_by('-created_at'))
for k in kits_list:
k.item_type = 'kit'
# Применяем фильтр по типу
products_list = []
kits_list = []
if type_filter in ['all', 'products']:
products_list = list(products.order_by('-created_at'))
for p in products_list:
p.item_type = 'product'
if type_filter in ['all', 'kits']:
kits_list = list(kits.order_by('-created_at'))
for k in kits_list:
k.item_type = 'kit'
# Объединяем и сортируем по дате создания
combined = sorted(
@@ -315,16 +336,25 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Получаем список статусов из модели
from ..models.base import BaseProductEntity
item_statuses = BaseProductEntity.STATUS_CHOICES
# Данные для фильтров
context['filters'] = {
'categories': ProductCategory.objects.filter(is_active=True),
'tags': ProductTag.objects.all(),
'tags': ProductTag.objects.filter(is_active=True),
'current': {
'search': self.request.GET.get('search', ''),
'category': self.request.GET.get('category', ''),
'status': self.request.GET.get('status', ''),
'type': self.request.GET.get('type', 'all'),
'in_stock': self.request.GET.get('in_stock', ''),
'tags': [int(tag) for tag in self.request.GET.getlist('tags') if tag.isdigit()],
}
}
context['item_statuses'] = item_statuses
# Кнопки действий
action_buttons = []