""" 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': 'Вариант установлен как по умолчанию'})