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:
2025-10-29 23:33:11 +03:00
parent 2341cf57c1
commit 9ff1f2d184
11 changed files with 1084 additions and 59 deletions

View 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