Files
octopus/myproject/products/views/configurablekit_views.py
Andrey Smakotin c4260f6b1c Добавлена модель атрибутов для вариативных товаров (ConfigurableKitProductAttribute)
- Создана модель ConfigurableKitProductAttribute с полями name, option, position, visible
- Добавлены формы и formsets для управления атрибутами родительского товара
- Обновлены CRUD представления для работы с атрибутами (создание/редактирование)
- Добавлен блок атрибутов в шаблоны создания/редактирования
- Обновлена страница детального просмотра с отображением атрибутов товара
- Добавлен JavaScript для динамического добавления форм атрибутов
- Реализована валидация дубликатов атрибутов в formset
- Атрибуты сохраняются в transaction.atomic() вместе с вариантами

Теперь можно определять схему атрибутов для экспорта на WooCommerce без использования JSON или ID, только name и option.
2025-11-18 09:24:49 +03:00

368 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
CRUD представления для вариативных товаров (ConfigurableKitProduct).
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.db.models import Q, Prefetch
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from django.db import transaction
from ..models import ConfigurableKitProduct, ConfigurableKitOption, ProductKit, ConfigurableKitProductAttribute
from ..forms import (
ConfigurableKitProductForm,
ConfigurableKitOptionFormSetCreate,
ConfigurableKitOptionFormSetUpdate,
ConfigurableKitProductAttributeFormSetCreate,
ConfigurableKitProductAttributeFormSetUpdate
)
class ConfigurableKitProductListView(LoginRequiredMixin, ListView):
model = ConfigurableKitProduct
template_name = 'products/configurablekit_list.html'
context_object_name = 'configurable_kits'
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset().prefetch_related(
Prefetch(
'options',
queryset=ConfigurableKitOption.objects.select_related('kit')
)
)
# Поиск
search_query = self.request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(name__icontains=search_query) |
Q(sku__icontains=search_query) |
Q(description__icontains=search_query)
)
# Фильтр по статусу
status_filter = self.request.GET.get('status')
if status_filter:
queryset = queryset.filter(status=status_filter)
return queryset.order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Данные для фильтров
context['filters'] = {
'current': {
'search': self.request.GET.get('search', ''),
'status': self.request.GET.get('status', ''),
}
}
# Кнопки действий
action_buttons = []
if self.request.user.has_perm('products.add_configurablekitproduct'):
action_buttons.append({
'url': reverse_lazy('products:configurablekit-create'),
'text': 'Создать вариативный товар',
'class': 'btn-primary',
'icon': 'plus-circle'
})
context['action_buttons'] = action_buttons
return context
class ConfigurableKitProductDetailView(LoginRequiredMixin, DetailView):
model = ConfigurableKitProduct
template_name = 'products/configurablekit_detail.html'
context_object_name = 'configurable_kit'
def get_queryset(self):
return super().get_queryset().prefetch_related(
Prefetch(
'options',
queryset=ConfigurableKitOption.objects.select_related('kit').order_by('id')
),
'parent_attributes'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Добавляем доступные комплекты для выбора (активные, не временные)
context['available_kits'] = ProductKit.objects.filter(
status='active',
is_temporary=False
).order_by('name')
return context
class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
model = ConfigurableKitProduct
form_class = ConfigurableKitProductForm
template_name = 'products/configurablekit_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Formset для вариантов
if 'option_formset' in kwargs:
context['option_formset'] = kwargs['option_formset']
elif self.request.POST:
context['option_formset'] = ConfigurableKitOptionFormSetCreate(
self.request.POST,
prefix='options'
)
else:
context['option_formset'] = ConfigurableKitOptionFormSetCreate(
prefix='options'
)
# Formset для атрибутов родителя
if 'attribute_formset' in kwargs:
context['attribute_formset'] = kwargs['attribute_formset']
elif self.request.POST:
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetCreate(
self.request.POST,
prefix='attributes'
)
else:
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetCreate(
prefix='attributes'
)
return context
def form_valid(self, form):
# Пересоздаём formsets с POST данными
option_formset = ConfigurableKitOptionFormSetCreate(
self.request.POST,
prefix='options'
)
attribute_formset = ConfigurableKitProductAttributeFormSetCreate(
self.request.POST,
prefix='attributes'
)
if not form.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в основной форме.')
return self.form_invalid(form)
if not option_formset.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в вариантах.')
return self.render_to_response(self.get_context_data(form=form, option_formset=option_formset, attribute_formset=attribute_formset))
if not attribute_formset.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в атрибутах.')
return self.render_to_response(self.get_context_data(form=form, option_formset=option_formset, attribute_formset=attribute_formset))
try:
with transaction.atomic():
# Сохраняем основную форму
self.object = form.save()
# Сохраняем варианты
option_formset.instance = self.object
option_formset.save()
# Сохраняем атрибуты родителя
attribute_formset.instance = self.object
attribute_formset.save()
messages.success(self.request, f'Вариативный товар "{self.object.name}" успешно создан!')
return super().form_valid(form)
except Exception as e:
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
return self.form_invalid(form)
def get_success_url(self):
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
model = ConfigurableKitProduct
form_class = ConfigurableKitProductForm
template_name = 'products/configurablekit_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Formset для вариантов
if 'option_formset' in kwargs:
context['option_formset'] = kwargs['option_formset']
elif self.request.POST:
context['option_formset'] = ConfigurableKitOptionFormSetUpdate(
self.request.POST,
instance=self.object,
prefix='options'
)
else:
context['option_formset'] = ConfigurableKitOptionFormSetUpdate(
instance=self.object,
prefix='options'
)
# Formset для атрибутов родителя
if 'attribute_formset' in kwargs:
context['attribute_formset'] = kwargs['attribute_formset']
elif self.request.POST:
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetUpdate(
self.request.POST,
instance=self.object,
prefix='attributes'
)
else:
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetUpdate(
instance=self.object,
prefix='attributes'
)
return context
def form_valid(self, form):
# Пересоздаём formsets с POST данными
option_formset = ConfigurableKitOptionFormSetUpdate(
self.request.POST,
instance=self.object,
prefix='options'
)
attribute_formset = ConfigurableKitProductAttributeFormSetUpdate(
self.request.POST,
instance=self.object,
prefix='attributes'
)
if not form.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в основной форме.')
return self.form_invalid(form)
if not option_formset.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в вариантах.')
return self.render_to_response(self.get_context_data(form=form, option_formset=option_formset, attribute_formset=attribute_formset))
if not attribute_formset.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в атрибутах.')
return self.render_to_response(self.get_context_data(form=form, option_formset=option_formset, attribute_formset=attribute_formset))
try:
with transaction.atomic():
# Сохраняем основную форму
self.object = form.save()
# Сохраняем варианты
option_formset.save()
# Сохраняем атрибуты родителя
attribute_formset.save()
messages.success(self.request, f'Вариативный товар "{self.object.name}" успешно обновлён!')
return super().form_valid(form)
except Exception as e:
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
return self.form_invalid(form)
def get_success_url(self):
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
class ConfigurableKitProductDeleteView(LoginRequiredMixin, DeleteView):
model = ConfigurableKitProduct
template_name = 'products/configurablekit_confirm_delete.html'
success_url = reverse_lazy('products:configurablekit-list')
def form_valid(self, form):
messages.success(self.request, f'Вариативный товар "{self.object.name}" успешно удалён!')
return super().form_valid(form)
# API для управления вариантами
@login_required
@require_POST
def add_option_to_configurable(request, pk):
"""
Добавить вариант (комплект) к вариативному товару.
"""
configurable = get_object_or_404(ConfigurableKitProduct, pk=pk)
kit_id = request.POST.get('kit_id')
attributes = request.POST.get('attributes', '')
is_default = request.POST.get('is_default') == 'true'
if not kit_id:
return JsonResponse({'success': False, 'error': 'Не указан комплект'}, status=400)
try:
kit = ProductKit.objects.get(pk=kit_id)
except ProductKit.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Комплект не найден'}, status=404)
# Проверяем, не добавлен ли уже этот комплект
if ConfigurableKitOption.objects.filter(parent=configurable, kit=kit).exists():
return JsonResponse({'success': False, 'error': 'Этот комплект уже добавлен как вариант'}, status=400)
# Если is_default=True, снимаем флаг с других
if is_default:
ConfigurableKitOption.objects.filter(parent=configurable, is_default=True).update(is_default=False)
# Создаём вариант
option = ConfigurableKitOption.objects.create(
parent=configurable,
kit=kit,
attributes=attributes,
is_default=is_default
)
return JsonResponse({
'success': True,
'option': {
'id': option.id,
'kit_id': kit.id,
'kit_name': kit.name,
'kit_sku': kit.sku or '',
'kit_price': str(kit.actual_price),
'attributes': option.attributes or '',
'is_default': option.is_default,
}
})
@login_required
@require_POST
def remove_option_from_configurable(request, pk, option_id):
"""
Удалить вариант из вариативного товара.
"""
configurable = get_object_or_404(ConfigurableKitProduct, pk=pk)
option = get_object_or_404(ConfigurableKitOption, pk=option_id, parent=configurable)
option.delete()
return JsonResponse({'success': True, 'message': 'Вариант удалён'})
@login_required
@require_POST
def set_option_as_default(request, pk, option_id):
"""
Установить вариант как по умолчанию.
"""
configurable = get_object_or_404(ConfigurableKitProduct, pk=pk)
option = get_object_or_404(ConfigurableKitOption, pk=option_id, parent=configurable)
# Снимаем флаг со всех других
ConfigurableKitOption.objects.filter(parent=configurable).update(is_default=False)
# Устанавливаем текущий
option.is_default = True
option.save(update_fields=['is_default'])
return JsonResponse({'success': True, 'message': 'Вариант установлен как по умолчанию'})