Переименование файлов configurablekit_* → configurableproduct_*
- Шаблоны: configurablekit_*.html → configurableproduct_*.html - Views: configurablekit_views.py → configurableproduct_views.py - JS: configurablekit_detail.js → configurableproduct_detail.js - Обновлены все template_name и импорты 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
747
myproject/products/views/configurableproduct_views.py
Normal file
747
myproject/products/views/configurableproduct_views.py
Normal file
@@ -0,0 +1,747 @@
|
||||
"""
|
||||
CRUD представления для вариативных товаров (ConfigurableProduct).
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
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 user_roles.mixins import ManagerOwnerRequiredMixin
|
||||
from ..models import ConfigurableProduct, ConfigurableProductOption, ProductKit, ConfigurableProductAttribute
|
||||
from ..forms import (
|
||||
ConfigurableProductForm,
|
||||
ConfigurableProductOptionFormSetCreate,
|
||||
ConfigurableProductOptionFormSetUpdate,
|
||||
ConfigurableProductAttributeFormSetCreate,
|
||||
ConfigurableProductAttributeFormSetUpdate
|
||||
)
|
||||
|
||||
|
||||
class ConfigurableProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
|
||||
model = ConfigurableProduct
|
||||
template_name = 'products/configurableproduct_list.html'
|
||||
context_object_name = 'configurable_kits'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().prefetch_related(
|
||||
Prefetch(
|
||||
'options',
|
||||
queryset=ConfigurableProductOption.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 ConfigurableProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
|
||||
model = ConfigurableProduct
|
||||
template_name = 'products/configurableproduct_detail.html'
|
||||
context_object_name = 'configurable_kit'
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related(
|
||||
Prefetch(
|
||||
'options',
|
||||
queryset=ConfigurableProductOption.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 ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
|
||||
model = ConfigurableProduct
|
||||
form_class = ConfigurableProductForm
|
||||
template_name = 'products/configurableproduct_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'] = ConfigurableProductOptionFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='options'
|
||||
)
|
||||
else:
|
||||
context['option_formset'] = ConfigurableProductOptionFormSetCreate(
|
||||
prefix='options'
|
||||
)
|
||||
|
||||
# Formset для атрибутов родителя
|
||||
if 'attribute_formset' in kwargs:
|
||||
context['attribute_formset'] = kwargs['attribute_formset']
|
||||
elif self.request.POST:
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='attributes'
|
||||
)
|
||||
else:
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetCreate(
|
||||
prefix='attributes'
|
||||
)
|
||||
|
||||
# Доступные комплекты для JavaScript (для выбора при добавлении значений атрибутов)
|
||||
context['available_kits'] = ProductKit.objects.filter(
|
||||
status='active',
|
||||
is_temporary=False
|
||||
).order_by('name')
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
from products.models.kits import ConfigurableProductOptionAttribute
|
||||
|
||||
# Пересоздаём formsets с POST данными
|
||||
option_formset = ConfigurableProductOptionFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='options'
|
||||
)
|
||||
attribute_formset = ConfigurableProductAttributeFormSetCreate(
|
||||
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():
|
||||
# Логирование ошибок formset
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Option formset errors: {option_formset.errors}")
|
||||
logger.error(f"Option formset non-form errors: {option_formset.non_form_errors()}")
|
||||
|
||||
# Показываем детальные ошибки
|
||||
error_msg = 'Ошибки в вариантах:\n'
|
||||
for i, form_errors in enumerate(option_formset.errors):
|
||||
if form_errors:
|
||||
error_msg += f' Вариант {i+1}: {form_errors}\n'
|
||||
if option_formset.non_form_errors():
|
||||
error_msg += f' Общие ошибки: {option_formset.non_form_errors()}\n'
|
||||
|
||||
messages.error(self.request, error_msg)
|
||||
return self.render_to_response(self.get_context_data(form=form, option_formset=option_formset, attribute_formset=attribute_formset))
|
||||
|
||||
# Валидация что каждый вариант имеет выбранный комплект
|
||||
validation_errors = self._validate_variant_kits(option_formset)
|
||||
if validation_errors:
|
||||
for error in validation_errors:
|
||||
messages.error(self.request, error)
|
||||
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
|
||||
for option_form in option_formset:
|
||||
if option_form.cleaned_data and not self._should_delete_form(option_form, option_formset):
|
||||
# Сохраняем сам вариант
|
||||
option = option_form.save(commit=False)
|
||||
option.parent = self.object
|
||||
option.save()
|
||||
|
||||
# Очищаем старые атрибуты варианта (если редактируем)
|
||||
option.attributes_set.all().delete()
|
||||
|
||||
# Сохраняем выбранные атрибуты для этого варианта
|
||||
for field_name, field_value in option_form.cleaned_data.items():
|
||||
if field_name.startswith('attribute_') and field_value:
|
||||
ConfigurableProductOptionAttribute.objects.create(
|
||||
option=option,
|
||||
attribute=field_value
|
||||
)
|
||||
elif self._should_delete_form(option_form, option_formset):
|
||||
# Удаляем вариант если помечен для удаления
|
||||
if option_form.instance.pk:
|
||||
option_form.instance.delete()
|
||||
|
||||
# Сохраняем атрибуты родителя - новый интерфейс
|
||||
# Карточный интерфейс: значения приходят как инлайн input'ы
|
||||
self._save_attributes_from_cards()
|
||||
|
||||
messages.success(self.request, f'Вариативный товар "{self.object.name}" успешно создан!')
|
||||
return super().form_valid(form)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return self.form_invalid(form)
|
||||
|
||||
def _save_attributes_from_cards(self):
|
||||
"""
|
||||
Сохранить атрибуты из карточного интерфейса.
|
||||
|
||||
Каждая карточка содержит:
|
||||
- attributes-X-name: название параметра
|
||||
- attributes-X-position: позиция
|
||||
- attributes-X-visible: видимость
|
||||
- attributes-X-DELETE: помечен ли для удаления
|
||||
- attributes-X-values: JSON массив значений параметра
|
||||
- attributes-X-kits: JSON массив ID комплектов для каждого значения
|
||||
"""
|
||||
import json
|
||||
from products.models.kits import ProductKit
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableProductAttribute.objects.filter(parent=self.object).delete()
|
||||
|
||||
# Получаем количество карточек параметров
|
||||
total_forms_str = self.request.POST.get('attributes-TOTAL_FORMS', '0')
|
||||
try:
|
||||
total_forms = int(total_forms_str)
|
||||
except (ValueError, TypeError):
|
||||
total_forms = 0
|
||||
|
||||
# Обрабатываем каждую карточку параметра
|
||||
for idx in range(total_forms):
|
||||
# Пропускаем если карточка помечена для удаления
|
||||
delete_key = f'attributes-{idx}-DELETE'
|
||||
if delete_key in self.request.POST and self.request.POST.get(delete_key):
|
||||
continue
|
||||
|
||||
# Получаем название параметра
|
||||
name = self.request.POST.get(f'attributes-{idx}-name', '').strip()
|
||||
if not name:
|
||||
continue
|
||||
|
||||
position = self.request.POST.get(f'attributes-{idx}-position', idx)
|
||||
try:
|
||||
position = int(position)
|
||||
except (ValueError, TypeError):
|
||||
position = idx
|
||||
|
||||
visible = self.request.POST.get(f'attributes-{idx}-visible') == 'on'
|
||||
|
||||
# Получаем значения и их привязанные комплекты
|
||||
values_json = self.request.POST.get(f'attributes-{idx}-values', '[]')
|
||||
kits_json = self.request.POST.get(f'attributes-{idx}-kits', '[]')
|
||||
|
||||
try:
|
||||
values = json.loads(values_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
values = []
|
||||
|
||||
try:
|
||||
kit_ids = json.loads(kits_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
|
||||
# Создаём ConfigurableProductAttribute для каждого значения
|
||||
for value_idx, value in enumerate(values):
|
||||
if value and value.strip():
|
||||
# Получаем соответствующий ID комплекта
|
||||
kit_id = kit_ids[value_idx] if value_idx < len(kit_ids) else None
|
||||
|
||||
# Приготавливаем параметры создания
|
||||
create_kwargs = {
|
||||
'parent': self.object,
|
||||
'name': name,
|
||||
'option': value.strip(),
|
||||
'position': position,
|
||||
'visible': visible
|
||||
}
|
||||
|
||||
# Добавляем комплект если указан
|
||||
if kit_id:
|
||||
try:
|
||||
kit = ProductKit.objects.get(id=kit_id)
|
||||
create_kwargs['kit'] = kit
|
||||
except ProductKit.DoesNotExist:
|
||||
# Комплект не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableProductAttribute.objects.create(**create_kwargs)
|
||||
|
||||
def _validate_variant_kits(self, option_formset):
|
||||
"""
|
||||
Валидация что каждый вариант имеет выбранный комплект.
|
||||
Возвращает список ошибок (пустой список если нет ошибок).
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for idx, option_form in enumerate(option_formset):
|
||||
# Пропускаем удаленные или пустые формы
|
||||
if not option_form.cleaned_data or self._should_delete_form(option_form, option_formset):
|
||||
continue
|
||||
|
||||
# Получаем kit_id из POST данных (он там должен быть установлен JavaScript'ом)
|
||||
kit_id = self.request.POST.get(f'options-{idx}-kit', '').strip()
|
||||
|
||||
if not kit_id:
|
||||
# Пытаемся получить из cleaned_data
|
||||
kit_id = option_form.cleaned_data.get('kit')
|
||||
|
||||
if not kit_id:
|
||||
# Если у варианта есть выбранные атрибуты, но нет комплекта - это ошибка
|
||||
has_attributes = any(
|
||||
option_form.cleaned_data.get(k)
|
||||
for k in option_form.cleaned_data.keys()
|
||||
if k.startswith('attribute_')
|
||||
)
|
||||
|
||||
if has_attributes:
|
||||
# Собираем названия выбранных атрибутов для сообщения об ошибке
|
||||
selected_attrs = [
|
||||
str(option_form.cleaned_data.get(k))
|
||||
for k in option_form.cleaned_data.keys()
|
||||
if k.startswith('attribute_') and option_form.cleaned_data.get(k)
|
||||
]
|
||||
errors.append(
|
||||
f'Вариант {idx + 1} ({", ".join(selected_attrs)}): '
|
||||
f'не выбран комплект. Пожалуйста, выберите значения атрибутов которые '
|
||||
f'привязаны к одному комплекту.'
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@staticmethod
|
||||
def _should_delete_form(form, formset):
|
||||
"""Проверить должна ли форма быть удалена"""
|
||||
if not formset.can_delete:
|
||||
return False
|
||||
# Проверяем поле DELETE (стандартное имя для formset deletion field)
|
||||
deletion_field_name = 'DELETE'
|
||||
if hasattr(formset, 'deletion_field') and hasattr(formset.deletion_field, 'name'):
|
||||
deletion_field_name = formset.deletion_field.name
|
||||
return form.cleaned_data.get(deletion_field_name, False)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class ConfigurableProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
|
||||
model = ConfigurableProduct
|
||||
form_class = ConfigurableProductForm
|
||||
template_name = 'products/configurableproduct_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'] = ConfigurableProductOptionFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='options'
|
||||
)
|
||||
else:
|
||||
context['option_formset'] = ConfigurableProductOptionFormSetUpdate(
|
||||
instance=self.object,
|
||||
prefix='options'
|
||||
)
|
||||
|
||||
# Formset для атрибутов родителя
|
||||
if 'attribute_formset' in kwargs:
|
||||
context['attribute_formset'] = kwargs['attribute_formset']
|
||||
elif self.request.POST:
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='attributes'
|
||||
)
|
||||
else:
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetUpdate(
|
||||
instance=self.object,
|
||||
prefix='attributes'
|
||||
)
|
||||
|
||||
# Доступные комплекты для JavaScript (для выбора при добавлении значений атрибутов)
|
||||
context['available_kits'] = ProductKit.objects.filter(
|
||||
status='active',
|
||||
is_temporary=False
|
||||
).order_by('name')
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
from products.models.kits import ConfigurableProductOptionAttribute
|
||||
|
||||
# Пересоздаём formsets с POST данными
|
||||
option_formset = ConfigurableProductOptionFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='options'
|
||||
)
|
||||
attribute_formset = ConfigurableProductAttributeFormSetUpdate(
|
||||
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():
|
||||
# Логирование ошибок formset
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Option formset errors: {option_formset.errors}")
|
||||
logger.error(f"Option formset non-form errors: {option_formset.non_form_errors()}")
|
||||
|
||||
# Показываем детальные ошибки
|
||||
error_msg = 'Ошибки в вариантах:\n'
|
||||
for i, form_errors in enumerate(option_formset.errors):
|
||||
if form_errors:
|
||||
error_msg += f' Вариант {i+1}: {form_errors}\n'
|
||||
if option_formset.non_form_errors():
|
||||
error_msg += f' Общие ошибки: {option_formset.non_form_errors()}\n'
|
||||
|
||||
messages.error(self.request, error_msg)
|
||||
return self.render_to_response(self.get_context_data(form=form, option_formset=option_formset, attribute_formset=attribute_formset))
|
||||
|
||||
# Валидация что каждый вариант имеет выбранный комплект
|
||||
validation_errors = self._validate_variant_kits(option_formset)
|
||||
if validation_errors:
|
||||
for error in validation_errors:
|
||||
messages.error(self.request, error)
|
||||
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()
|
||||
|
||||
# Сохраняем варианты И их атрибуты
|
||||
for option_form in option_formset:
|
||||
if option_form.cleaned_data and not self._should_delete_form(option_form, option_formset):
|
||||
# Сохраняем сам вариант
|
||||
option = option_form.save(commit=False)
|
||||
option.parent = self.object
|
||||
option.save()
|
||||
|
||||
# Очищаем старые атрибуты варианта
|
||||
option.attributes_set.all().delete()
|
||||
|
||||
# Сохраняем выбранные атрибуты для этого варианта
|
||||
for field_name, field_value in option_form.cleaned_data.items():
|
||||
if field_name.startswith('attribute_') and field_value:
|
||||
ConfigurableProductOptionAttribute.objects.create(
|
||||
option=option,
|
||||
attribute=field_value
|
||||
)
|
||||
elif self._should_delete_form(option_form, option_formset):
|
||||
# Удаляем вариант если помечен для удаления
|
||||
if option_form.instance.pk:
|
||||
option_form.instance.delete()
|
||||
|
||||
# Сохраняем атрибуты родителя - новый интерфейс
|
||||
# Карточный интерфейс: значения приходят как инлайн input'ы
|
||||
self._save_attributes_from_cards()
|
||||
|
||||
messages.success(self.request, f'Вариативный товар "{self.object.name}" успешно обновлён!')
|
||||
return super().form_valid(form)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return self.form_invalid(form)
|
||||
|
||||
def _save_attributes_from_cards(self):
|
||||
"""
|
||||
Сохранить атрибуты из карточного интерфейса.
|
||||
|
||||
Каждая карточка содержит:
|
||||
- attributes-X-name: название параметра
|
||||
- attributes-X-position: позиция
|
||||
- attributes-X-visible: видимость
|
||||
- attributes-X-DELETE: помечен ли для удаления
|
||||
- attributes-X-values: JSON массив значений параметра
|
||||
- attributes-X-kits: JSON массив ID комплектов для каждого значения
|
||||
"""
|
||||
import json
|
||||
from products.models.kits import ProductKit
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableProductAttribute.objects.filter(parent=self.object).delete()
|
||||
|
||||
# Получаем количество карточек параметров
|
||||
total_forms_str = self.request.POST.get('attributes-TOTAL_FORMS', '0')
|
||||
try:
|
||||
total_forms = int(total_forms_str)
|
||||
except (ValueError, TypeError):
|
||||
total_forms = 0
|
||||
|
||||
# Обрабатываем каждую карточку параметра
|
||||
for idx in range(total_forms):
|
||||
# Пропускаем если карточка помечена для удаления
|
||||
delete_key = f'attributes-{idx}-DELETE'
|
||||
if delete_key in self.request.POST and self.request.POST.get(delete_key):
|
||||
continue
|
||||
|
||||
# Получаем название параметра
|
||||
name = self.request.POST.get(f'attributes-{idx}-name', '').strip()
|
||||
if not name:
|
||||
continue
|
||||
|
||||
position = self.request.POST.get(f'attributes-{idx}-position', idx)
|
||||
try:
|
||||
position = int(position)
|
||||
except (ValueError, TypeError):
|
||||
position = idx
|
||||
|
||||
visible = self.request.POST.get(f'attributes-{idx}-visible') == 'on'
|
||||
|
||||
# Получаем значения и их привязанные комплекты
|
||||
values_json = self.request.POST.get(f'attributes-{idx}-values', '[]')
|
||||
kits_json = self.request.POST.get(f'attributes-{idx}-kits', '[]')
|
||||
|
||||
try:
|
||||
values = json.loads(values_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
values = []
|
||||
|
||||
try:
|
||||
kit_ids = json.loads(kits_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
|
||||
# Создаём ConfigurableProductAttribute для каждого значения
|
||||
for value_idx, value in enumerate(values):
|
||||
if value and value.strip():
|
||||
# Получаем соответствующий ID комплекта
|
||||
kit_id = kit_ids[value_idx] if value_idx < len(kit_ids) else None
|
||||
|
||||
# Приготавливаем параметры создания
|
||||
create_kwargs = {
|
||||
'parent': self.object,
|
||||
'name': name,
|
||||
'option': value.strip(),
|
||||
'position': position,
|
||||
'visible': visible
|
||||
}
|
||||
|
||||
# Добавляем комплект если указан
|
||||
if kit_id:
|
||||
try:
|
||||
kit = ProductKit.objects.get(id=kit_id)
|
||||
create_kwargs['kit'] = kit
|
||||
except ProductKit.DoesNotExist:
|
||||
# Комплект не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableProductAttribute.objects.create(**create_kwargs)
|
||||
|
||||
def _validate_variant_kits(self, option_formset):
|
||||
"""
|
||||
Валидация что каждый вариант имеет выбранный комплект.
|
||||
Возвращает список ошибок (пустой список если нет ошибок).
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for idx, option_form in enumerate(option_formset):
|
||||
# Пропускаем удаленные или пустые формы
|
||||
if not option_form.cleaned_data or self._should_delete_form(option_form, option_formset):
|
||||
continue
|
||||
|
||||
# Получаем kit_id из POST данных (он там должен быть установлен JavaScript'ом)
|
||||
kit_id = self.request.POST.get(f'options-{idx}-kit', '').strip()
|
||||
|
||||
if not kit_id:
|
||||
# Пытаемся получить из cleaned_data
|
||||
kit_id = option_form.cleaned_data.get('kit')
|
||||
|
||||
if not kit_id:
|
||||
# Если у варианта есть выбранные атрибуты, но нет комплекта - это ошибка
|
||||
has_attributes = any(
|
||||
option_form.cleaned_data.get(k)
|
||||
for k in option_form.cleaned_data.keys()
|
||||
if k.startswith('attribute_')
|
||||
)
|
||||
|
||||
if has_attributes:
|
||||
# Собираем названия выбранных атрибутов для сообщения об ошибке
|
||||
selected_attrs = [
|
||||
str(option_form.cleaned_data.get(k))
|
||||
for k in option_form.cleaned_data.keys()
|
||||
if k.startswith('attribute_') and option_form.cleaned_data.get(k)
|
||||
]
|
||||
errors.append(
|
||||
f'Вариант {idx + 1} ({", ".join(selected_attrs)}): '
|
||||
f'не выбран комплект. Пожалуйста, выберите значения атрибутов которые '
|
||||
f'привязаны к одному комплекту.'
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@staticmethod
|
||||
def _should_delete_form(form, formset):
|
||||
"""Проверить должна ли форма быть удалена"""
|
||||
if not formset.can_delete:
|
||||
return False
|
||||
# Проверяем поле DELETE (стандартное имя для formset deletion field)
|
||||
deletion_field_name = 'DELETE'
|
||||
if hasattr(formset, 'deletion_field') and hasattr(formset.deletion_field, 'name'):
|
||||
deletion_field_name = formset.deletion_field.name
|
||||
return form.cleaned_data.get(deletion_field_name, False)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class ConfigurableProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DeleteView):
|
||||
model = ConfigurableProduct
|
||||
template_name = 'products/configurableproduct_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(ConfigurableProduct, 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 ConfigurableProductOption.objects.filter(parent=configurable, kit=kit).exists():
|
||||
return JsonResponse({'success': False, 'error': 'Этот комплект уже добавлен как вариант'}, status=400)
|
||||
|
||||
# Если is_default=True, снимаем флаг с других
|
||||
if is_default:
|
||||
ConfigurableProductOption.objects.filter(parent=configurable, is_default=True).update(is_default=False)
|
||||
|
||||
# Создаём вариант
|
||||
option = ConfigurableProductOption.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(ConfigurableProduct, pk=pk)
|
||||
option = get_object_or_404(ConfigurableProductOption, 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(ConfigurableProduct, pk=pk)
|
||||
option = get_object_or_404(ConfigurableProductOption, pk=option_id, parent=configurable)
|
||||
|
||||
# Снимаем флаг со всех других
|
||||
ConfigurableProductOption.objects.filter(parent=configurable).update(is_default=False)
|
||||
|
||||
# Устанавливаем текущий
|
||||
option.is_default = True
|
||||
option.save(update_fields=['is_default'])
|
||||
|
||||
return JsonResponse({'success': True, 'message': 'Вариант установлен как по умолчанию'})
|
||||
Reference in New Issue
Block a user