feat: Добавить отображение наличия товаров и цены вариантов во все шаблоны CRUD
Добавлена визуализация статуса наличия (in_stock) и цены вариантов во все CRUD шаблоны товаров и групп вариантов.
Product (товары):
- product_list.html: добавлена колонка "В наличии" с бейджами (зелёный/красный)
- product_detail.html: добавлена строка "В наличии" в таблицу данных товара
- product_form.html: добавлена информационная секция о наличии при редактировании
- all_products_list.html: добавлена колонка "В наличии" для товаров
- productkit_list.html: обновлены стили бейджей статуса
ProductVariantGroup (группы вариантов):
- variantgroup_list.html: добавлены колонки "В наличии" и "Цена" в таблицу групп
- variantgroup_detail.html: добавлены отображение наличия и цены в информационный блок слева
- variantgroup_detail.html: добавлена колонка "В наличии" в таблицу товаров группы
- variantgroup_form.html: добавлены отображение артикула, цены и статуса наличия в formset таблице
- variantgroup_form.html: добавлен JavaScript код для динамического обновления данных товара при выборе через Select2
- variantgroup_confirm_delete.html: добавлена информация о наличии и цене группы в окно подтверждения удаления
Views optimization:
- ProductVariantGroupListView: добавлен prefetch_related('items__product') для оптимизации N+1 запросов
- Все представления используют оптимизированные запросы для вычисления in_stock и price свойств
UI/UX улучшения:
- Используются Bootstrap 5 бейджи с иконками (bg-success/bg-danger)
- Визуальное выделение статуса наличия через цвет и значки
- Информативное отображение цены варианта во всех местах
- Динамическое обновление информации при выборе товаров в formset
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
292
myproject/products/views/variant_group_views.py
Normal file
292
myproject/products/views/variant_group_views.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
CRUD представления для групп вариантов товаров (ProductVariantGroup).
|
||||
Включает управление товарами в группе с приоритизацией.
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q, Count
|
||||
from django.db import transaction
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from ..models import ProductVariantGroup, ProductVariantGroupItem
|
||||
from ..forms import (
|
||||
ProductVariantGroupForm,
|
||||
ProductVariantGroupItemFormSetCreate,
|
||||
ProductVariantGroupItemFormSetUpdate
|
||||
)
|
||||
|
||||
|
||||
class ProductVariantGroupListView(LoginRequiredMixin, ListView):
|
||||
"""Список всех групп вариантов с поиском и фильтрацией"""
|
||||
model = ProductVariantGroup
|
||||
template_name = 'products/variantgroup_list.html'
|
||||
context_object_name = 'variant_groups'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Оптимизация: загружаем товары для вычисления in_stock и price
|
||||
queryset = queryset.prefetch_related('items__product')
|
||||
|
||||
# Добавляем количество товаров в каждую группу
|
||||
queryset = queryset.annotate(items_count=Count('items'))
|
||||
|
||||
# Поиск по названию и описанию
|
||||
search_query = self.request.GET.get('search')
|
||||
if search_query:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(description__icontains=search_query)
|
||||
)
|
||||
|
||||
return queryset.order_by('-updated_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
return context
|
||||
|
||||
|
||||
class ProductVariantGroupCreateView(LoginRequiredMixin, CreateView):
|
||||
"""Создание новой группы вариантов с добавлением товаров"""
|
||||
model = ProductVariantGroup
|
||||
form_class = ProductVariantGroupForm
|
||||
template_name = 'products/variantgroup_form.html'
|
||||
success_url = reverse_lazy('products:variantgroup-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context['items_formset'] = ProductVariantGroupItemFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='items'
|
||||
)
|
||||
else:
|
||||
context['items_formset'] = ProductVariantGroupItemFormSetCreate(
|
||||
prefix='items'
|
||||
)
|
||||
context['is_create'] = True
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Сохраняем группу и товары в одной транзакции"""
|
||||
items_formset = ProductVariantGroupItemFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='items'
|
||||
)
|
||||
|
||||
# Проверяем валидность основной формы и формсета
|
||||
if not form.is_valid() or not items_formset.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Сохраняем группу
|
||||
self.object = form.save(commit=True)
|
||||
|
||||
# Сохраняем товары
|
||||
items_formset.instance = self.object
|
||||
items_formset.save()
|
||||
|
||||
# Пересчитываем приоритеты
|
||||
self._recalculate_priorities(self.object)
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
f'Группа вариантов "{self.object.name}" успешно создана!'
|
||||
)
|
||||
return redirect('products:variantgroup-list')
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
return self.form_invalid(form)
|
||||
|
||||
@staticmethod
|
||||
def _recalculate_priorities(variant_group):
|
||||
"""Пересчитывает приоритеты товаров в группе (по порядку в БД)"""
|
||||
items = variant_group.items.all().order_by('id')
|
||||
for idx, item in enumerate(items, start=1):
|
||||
item.priority = idx
|
||||
item.save(update_fields=['priority'])
|
||||
|
||||
|
||||
class ProductVariantGroupDetailView(LoginRequiredMixin, DetailView):
|
||||
"""Просмотр группы вариантов с список товаров в ней"""
|
||||
model = ProductVariantGroup
|
||||
template_name = 'products/variantgroup_detail.html'
|
||||
context_object_name = 'variant_group'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
return queryset.prefetch_related('items__product')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Получаем товары с приоритетами
|
||||
context['items'] = self.object.items.all().select_related('product').order_by('priority')
|
||||
return context
|
||||
|
||||
|
||||
class ProductVariantGroupUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""Редактирование группы вариантов и управление товарами в ней"""
|
||||
model = ProductVariantGroup
|
||||
form_class = ProductVariantGroupForm
|
||||
template_name = 'products/variantgroup_form.html'
|
||||
success_url = reverse_lazy('products:variantgroup-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context['items_formset'] = ProductVariantGroupItemFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='items'
|
||||
)
|
||||
else:
|
||||
context['items_formset'] = ProductVariantGroupItemFormSetUpdate(
|
||||
instance=self.object,
|
||||
prefix='items'
|
||||
)
|
||||
context['is_create'] = False
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Сохраняем изменения группы и товаров"""
|
||||
items_formset = ProductVariantGroupItemFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='items'
|
||||
)
|
||||
|
||||
if not form.is_valid() or not items_formset.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
self.object = form.save(commit=True)
|
||||
items_formset.instance = self.object
|
||||
items_formset.save()
|
||||
|
||||
# Пересчитываем приоритеты после редактирования
|
||||
self._recalculate_priorities(self.object)
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
f'Группа вариантов "{self.object.name}" успешно обновлена!'
|
||||
)
|
||||
return redirect('products:variantgroup-list')
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
return self.form_invalid(form)
|
||||
|
||||
@staticmethod
|
||||
def _recalculate_priorities(variant_group):
|
||||
"""Пересчитывает приоритеты товаров в группе"""
|
||||
items = variant_group.items.all().order_by('id')
|
||||
for idx, item in enumerate(items, start=1):
|
||||
item.priority = idx
|
||||
item.save(update_fields=['priority'])
|
||||
|
||||
|
||||
class ProductVariantGroupDeleteView(LoginRequiredMixin, DeleteView):
|
||||
"""Удаление группы вариантов с подтверждением"""
|
||||
model = ProductVariantGroup
|
||||
template_name = 'products/variantgroup_confirm_delete.html'
|
||||
success_url = reverse_lazy('products:variantgroup-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Добавляем информацию о товарах в группе
|
||||
context['items'] = self.object.items.all().select_related('product').order_by('priority')
|
||||
context['items_count'] = context['items'].count()
|
||||
return context
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Удаляем группу и показываем сообщение об успехе"""
|
||||
group_name = self.get_object().name
|
||||
response = super().delete(request, *args, **kwargs)
|
||||
messages.success(request, f'Группа вариантов "{group_name}" успешно удалена!')
|
||||
return response
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@login_required
|
||||
def product_variant_group_item_move(request, item_id, direction):
|
||||
"""
|
||||
AJAX view для перемещения товара вверх/вниз в группе (UP/DOWN кнопки).
|
||||
Перемещает товар вверх (up) или вниз (down) в списке приоритетов.
|
||||
Автоматически пересчитывает приоритеты.
|
||||
"""
|
||||
try:
|
||||
# Получаем товар в группе
|
||||
item = get_object_or_404(ProductVariantGroupItem, pk=item_id)
|
||||
variant_group = item.variant_group
|
||||
|
||||
# Получаем все товары в группе отсортированные по приоритету
|
||||
all_items = list(
|
||||
variant_group.items.all()
|
||||
.order_by('priority')
|
||||
.values_list('id', 'priority')
|
||||
)
|
||||
|
||||
# Находим текущую позицию товара
|
||||
current_index = next(
|
||||
(idx for idx, (item_pk, _) in enumerate(all_items) if item_pk == item_id),
|
||||
None
|
||||
)
|
||||
|
||||
if current_index is None:
|
||||
return JsonResponse({'error': 'Товар не найден'}, status=404)
|
||||
|
||||
# Перемещаем товар
|
||||
if direction == 'up' and current_index > 0:
|
||||
# Меняем местами с товаром выше
|
||||
all_items[current_index], all_items[current_index - 1] = \
|
||||
all_items[current_index - 1], all_items[current_index]
|
||||
elif direction == 'down' and current_index < len(all_items) - 1:
|
||||
# Меняем местами с товаром ниже
|
||||
all_items[current_index], all_items[current_index + 1] = \
|
||||
all_items[current_index + 1], all_items[current_index]
|
||||
else:
|
||||
# Товар уже в крайней позиции
|
||||
return JsonResponse({
|
||||
'error': f'Товар уже в крайней позиции',
|
||||
'items': _get_items_data(variant_group)
|
||||
}, status=400)
|
||||
|
||||
# Пересчитываем приоритеты
|
||||
with transaction.atomic():
|
||||
for idx, (item_pk, _) in enumerate(all_items, start=1):
|
||||
ProductVariantGroupItem.objects.filter(pk=item_pk).update(priority=idx)
|
||||
|
||||
# Возвращаем обновленный список товаров
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Товар перемещен',
|
||||
'items': _get_items_data(variant_group)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
def _get_items_data(variant_group):
|
||||
"""Возвращает данные о товарах для обновления таблицы"""
|
||||
items = variant_group.items.all().select_related('product').order_by('priority')
|
||||
items_data = []
|
||||
for item in items:
|
||||
items_data.append({
|
||||
'id': item.id,
|
||||
'product_name': item.product.name,
|
||||
'product_sku': item.product.sku,
|
||||
'product_price': str(item.product.sale_price),
|
||||
'priority': item.priority,
|
||||
'can_move_up': item.priority > 1,
|
||||
'can_move_down': item.priority < items.count()
|
||||
})
|
||||
return items_data
|
||||
Reference in New Issue
Block a user