fix: Исправить сохранение множественных товаров в комплект

ПРОБЛЕМА:
При создании комплекта с несколькими товарами сохранялся только первый товар.

ПРИЧИНЫ И РЕШЕНИЯ:

1. Неправильный префикс в JavaScript (productkit_create.html)
   - Динамически добавляемые формы создавались с префиксом kititem_set-
   - Django ожидает префикс kititem-
   - ИСПРАВЛЕНО: изменены все name атрибуты с kititem_set- на kititem-

2. NULL constraint для quantity (models.py)
   - Поле KitItem.quantity было NOT NULL
   - Пустые формы пытались сохраняться с NULL
   - ИСПРАВЛЕНО: добавлены null=True, blank=True к полю quantity

3. Неправильная валидация пустых форм (forms.py)
   - Не было логики для обработки пустых компонентов
   - ИСПРАВЛЕНО: пустые формы получают quantity=None, заполненные требуют quantity>0

4. Неправильный порядок сохранения (productkit_views.py)
   - Формсет не имел правильного prefixсе
   - ИСПРАВЛЕНО: явно установлен prefix='kititem' везде (get_context_data, form_valid)

 РЕЗУЛЬТАТ: Теперь можно создавать комплекты с неограниченным количеством товаров

🧪 ТЕСТИРОВАНО:
- Комплект 0 товаров ✓
- Комплект 1 товар ✓
- Комплект 3 товара ✓

🤖 Generated with Claude Code
This commit is contained in:
2025-10-23 23:48:39 +03:00
parent 59ba375404
commit 2fb6253d06
5 changed files with 999 additions and 295 deletions

View File

@@ -83,38 +83,102 @@ class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
View для создания нового комплекта (только базовая информация).
После создания redirect на страницу редактирования для добавления товаров.
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})"
selected_products[key] = {
'id': product.id,
'text': text,
'price': str(product.sale_price) if product.sale_price else None
}
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()
# Сохраняем основную форму (комплект)
self.object = form.save(commit=True) # Явно сохраняем в БД
# Убеждаемся что объект в БД
if not self.object.pk:
raise Exception("Не удалось сохранить комплект в базу данных")
# Сохраняем компоненты
kititem_formset.instance = self.object
saved_items = kititem_formset.save()
# Обработка фотографий
handle_photos(self.request, self.object, ProductKitPhoto, 'kit')
messages.success(
self.request,
f'Комплект "{self.object.name}" создан! Теперь добавьте товары в комплект.'
f'Комплект "{self.object.name}" успешно создан!'
)
# Всегда redirect на страницу редактирования для добавления товаров
return redirect('products:productkit-update', pk=self.object.pk)
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 get_success_url(self):
# Этот метод не используется, т.к. мы делаем redirect в form_valid
return reverse_lazy('products:productkit-update', kwargs={'pk': self.object.pk})
def form_invalid(self, form):
# Получаем формсет для отображения ошибок
context = self.get_context_data(form=form)
return self.render_to_response(context)
class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
@@ -130,9 +194,9 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
context = super().get_context_data(**kwargs)
if self.request.POST:
context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object)
context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem')
else:
context['kititem_formset'] = KitItemFormSetUpdate(instance=self.object)
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()
@@ -140,38 +204,44 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
return context
def form_valid(self, form):
# Получаем формсет из POST
kititem_formset = KitItemFormSetUpdate(self.request.POST, instance=self.object)
# Получаем формсет из POST с правильным префиксом
kititem_formset = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem')
# Проверяем валидность формсета
if kititem_formset.is_valid():
try:
with transaction.atomic():
# Сохраняем основную форму
self.object = form.save()
# Проверяем валидность основной формы и формсета
if not form.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в основной форме комплекта.')
return self.form_invalid(form)
# Сохраняем компоненты
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)}')
return self.form_invalid(form)
else:
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)