Files
octopus/myproject/products/views/productkit_views.py
Andrey Smakotin 5d5de1fe31 Рефакторинг: перенос логики создания временных комплектов в сервис
Изменения:
- Удалена функция create_temporary_kit из myproject/orders/views.py
- Перенесена в новый сервис myproject/products/services/kit_service.py
- Добавлен API endpoint products:api-temporary-kit-create для создания временных комплектов
- Обновлены URL-ы соответственно

Преимущества:
- Логика временных комплектов теперь находится в соответствующем приложении (products)
- Упрощена архитектура orders приложения
- Сервис может быть переиспользован в других контекстах
- Лучшее разделение ответственности между приложениями

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 23:44:05 +03:00

388 lines
17 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 представления для комплектов товаров (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')
# Скрываем временные комплекты из общего каталога
queryset = queryset.filter(is_temporary=False)
# Поиск по названию
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 post(self, request, *args, **kwargs):
"""
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
API возвращает ID в формате "product_123" или "kit_456", но Django ожидает числа.
"""
# Создаем изменяемую копию POST данных
post_data = request.POST.copy()
# Очищаем product ID от префиксов (product_123 -> 123)
for key in post_data.keys():
if key.endswith('-product') and post_data[key]:
value = post_data[key]
if '_' in value:
# Извлекаем числовой ID из "product_123"
numeric_id = value.split('_')[1]
post_data[key] = numeric_id
# Заменяем request.POST на очищенные данные
request.POST = post_data
return super().post(request, *args, **kwargs)
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()
# Пересчитываем базовую цену после сохранения всех компонентов
self.object.recalculate_base_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 post(self, request, *args, **kwargs):
"""
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
API возвращает ID в формате "product_123" или "kit_456", но Django ожидает числа.
"""
# Создаем изменяемую копию POST данных
post_data = request.POST.copy()
# Очищаем product ID от префиксов (product_123 -> 123)
for key in post_data.keys():
if key.endswith('-product') and post_data[key]:
value = post_data[key]
if '_' in value:
# Извлекаем числовой ID из "product_123"
numeric_id = value.split('_')[1]
post_data[key] = numeric_id
# Заменяем request.POST на очищенные данные
request.POST = post_data
return super().post(request, *args, **kwargs)
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()
# Пересчитываем базовую цену после сохранения всех компонентов
self.object.recalculate_base_price()
# Обработка фотографий
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')
class ProductKitMakePermanentView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
View для преобразования временного комплекта в постоянный.
Позволяет отредактировать название, добавить категории, теги перед сохранением.
"""
model = ProductKit
template_name = 'products/productkit_make_permanent.html'
context_object_name = 'kit'
permission_required = 'products.change_productkit'
fields = ['name', 'description', 'categories', 'tags', 'sale_price']
def get_queryset(self):
# Только временные комплекты можно преобразовать
return super().get_queryset().filter(is_temporary=True)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['kit_items'] = self.object.kit_items.all().select_related('product', 'variant_group')
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
return context
def form_valid(self, form):
# Преобразуем в постоянный
if self.object.make_permanent():
messages.success(
self.request,
f'Комплект "{self.object.name}" преобразован в постоянный и теперь доступен в каталоге!'
)
else:
messages.warning(self.request, f'Комплект "{self.object.name}" уже является постоянным.')
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('products:productkit-detail', kwargs={'pk': self.object.pk})