""" CRUD представления для комплектов товаров (ProductKit). """ 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.shortcuts import redirect from django.db import transaction from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate from .utils import handle_photos class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = ProductKit template_name = 'products/productkit_list.html' context_object_name = 'kits' permission_required = 'products.view_productkit' paginate_by = 10 def get_queryset(self): queryset = super().get_queryset() queryset = queryset.prefetch_related('categories', 'photos', 'kit_items', 'tags') # Поиск по названию search_query = self.request.GET.get('search') if search_query: queryset = queryset.filter(name__icontains=search_query) # Фильтр по категории category_id = self.request.GET.get('category') if category_id: queryset = queryset.filter(categories__id=category_id) # Фильтр по статусу is_active = self.request.GET.get('is_active') if is_active == '1': queryset = queryset.filter(is_active=True) elif is_active == '0': queryset = queryset.filter(is_active=False) return queryset.order_by('-created_at') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Данные для фильтров context['filters'] = { 'categories': ProductCategory.objects.filter(is_active=True), 'tags': ProductTag.objects.all(), 'current': { 'search': self.request.GET.get('search', ''), 'category': self.request.GET.get('category', ''), 'is_active': self.request.GET.get('is_active', ''), 'tags': self.request.GET.getlist('tags'), } } # Кнопки действий action_buttons = [] if self.request.user.has_perm('products.add_productkit'): action_buttons.append({ 'url': reverse_lazy('products:productkit-create'), 'text': 'Создать комплект', 'class': 'btn-primary', 'icon': 'plus-circle' }) action_buttons.append({ 'url': reverse_lazy('products:product-list'), 'text': 'К товарам', 'class': 'btn-outline-primary', 'icon': 'box' }) context['action_buttons'] = action_buttons return context class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ View для создания нового комплекта с добавлением компонентов на одной странице. """ model = ProductKit form_class = ProductKitForm template_name = 'products/productkit_create.html' permission_required = 'products.add_productkit' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if self.request.POST: context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, prefix='kititem') # При ошибке валидации: извлекаем выбранные товары для предзагрузки в Select2 from ..models import Product, ProductVariantGroup selected_products = {} selected_variants = {} for key, value in self.request.POST.items(): if '-product' in key and value: try: product = Product.objects.get(id=value) text = product.name if product.sku: text += f" ({product.sku})" # Получаем actual_price: приоритет sale_price > price actual_price = product.sale_price if product.sale_price else product.price selected_products[key] = { 'id': product.id, 'text': text, 'price': str(product.price) if product.price else None, 'actual_price': str(actual_price) if actual_price else '0' } except Product.DoesNotExist: pass if '-variant_group' in key and value: try: variant_group = ProductVariantGroup.objects.get(id=value) selected_variants[key] = { 'id': variant_group.id, 'text': variant_group.name } except ProductVariantGroup.DoesNotExist: pass context['selected_products'] = selected_products context['selected_variants'] = selected_variants else: context['kititem_formset'] = KitItemFormSetCreate(prefix='kititem') return context def form_valid(self, form): # Получаем формсет из POST с правильным префиксом kititem_formset = KitItemFormSetCreate(self.request.POST, prefix='kititem') # Проверяем валидность основной формы if not form.is_valid(): messages.error(self.request, 'Пожалуйста, исправьте ошибки в основной форме комплекта.') return self.form_invalid(form) if not kititem_formset.is_valid(): # Если формсет невалиден, показываем форму с ошибками messages.error(self.request, 'Пожалуйста, исправьте ошибки в компонентах комплекта.') return self.form_invalid(form) try: with transaction.atomic(): # Сохраняем основную форму (комплект) self.object = form.save(commit=True) # Убеждаемся что объект в БД if not self.object.pk: raise Exception("Не удалось сохранить комплект в базу данных") # Сохраняем компоненты kititem_formset.instance = self.object saved_items = kititem_formset.save() # Валидация ценообразования больше не требуется в новой системе # (новая система использует простой расчёт на основе actual_price компонентов) # Обработка фотографий handle_photos(self.request, self.object, ProductKitPhoto, 'kit') messages.success( self.request, f'Комплект "{self.object.name}" успешно создан!' ) return redirect('products:productkit-list') except Exception as e: messages.error(self.request, f'Ошибка при сохранении: {str(e)}') import traceback traceback.print_exc() return self.form_invalid(form) def form_invalid(self, form): # Получаем формсет для отображения ошибок context = self.get_context_data(form=form) return self.render_to_response(context) class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ View для редактирования существующего комплекта и добавления товаров. """ model = ProductKit form_class = ProductKitForm template_name = 'products/productkit_edit.html' permission_required = 'products.change_productkit' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if self.request.POST: context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem') else: context['kititem_formset'] = KitItemFormSetUpdate(instance=self.object, prefix='kititem') context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at') context['photos_count'] = self.object.photos.count() return context def form_valid(self, form): # Получаем формсет из POST с правильным префиксом kititem_formset = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem') # Проверяем валидность основной формы и формсета if not form.is_valid(): messages.error(self.request, 'Пожалуйста, исправьте ошибки в основной форме комплекта.') return self.form_invalid(form) if not kititem_formset.is_valid(): # Если формсет невалиден, показываем форму с ошибками messages.error(self.request, 'Пожалуйста, исправьте ошибки в компонентах комплекта.') return self.form_invalid(form) try: with transaction.atomic(): # Сохраняем основную форму self.object = form.save(commit=True) # Сохраняем компоненты kititem_formset.instance = self.object kititem_formset.save() # Обработка фотографий handle_photos(self.request, self.object, ProductKitPhoto, 'kit') messages.success(self.request, f'Комплект "{self.object.name}" успешно обновлен!') # Проверяем, какую кнопку нажали if self.request.POST.get('action') == 'continue': return redirect('products:productkit-update', pk=self.object.pk) else: return redirect('products:productkit-list') except Exception as e: messages.error(self.request, f'Ошибка при сохранении: {str(e)}') import traceback traceback.print_exc() return self.form_invalid(form) def form_invalid(self, form): # Получаем формсет для отображения ошибок context = self.get_context_data(form=form) return self.render_to_response(context) def get_success_url(self): return reverse_lazy('products:productkit-list') class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ View для просмотра деталей комплекта. Показывает все компоненты, цены, фотографии. """ model = ProductKit template_name = 'products/productkit_detail.html' context_object_name = 'kit' permission_required = 'products.view_productkit' def get_queryset(self): # Prefetch для оптимизации запросов return super().get_queryset().prefetch_related( 'photos', 'kit_items__product', 'kit_items__variant_group', 'tags' ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Добавляем фотографии комплекта в контекст context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at') context['photos_count'] = self.object.photos.count() # Добавляем компоненты context['kit_items'] = self.object.kit_items.all().select_related('product', 'variant_group') return context class ProductKitDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): """ View для удаления комплекта. """ model = ProductKit template_name = 'products/productkit_confirm_delete.html' context_object_name = 'kit' permission_required = 'products.delete_productkit' def get_success_url(self): messages.success(self.request, f'Комплект "{self.object.name}" успешно удален!') return reverse_lazy('products:productkit-list')