- Add view mixins (RoleRequiredMixin, OwnerRequiredMixin, ManagerOwnerRequiredMixin) to user_roles/mixins.py - Replace PermissionRequiredMixin with ManagerOwnerRequiredMixin in all product views - Remove permission_required attributes from view classes - Owner and Manager roles now grant access without Django model permissions This allows owners to access all product functionality through their custom role, without needing to be superusers or have explicit Django permissions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
748 lines
33 KiB
Python
748 lines
33 KiB
Python
"""
|
||
CRUD представления для вариативных товаров (ConfigurableKitProduct).
|
||
"""
|
||
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 ConfigurableKitProduct, ConfigurableKitOption, ProductKit, ConfigurableKitProductAttribute
|
||
from ..forms import (
|
||
ConfigurableKitProductForm,
|
||
ConfigurableKitOptionFormSetCreate,
|
||
ConfigurableKitOptionFormSetUpdate,
|
||
ConfigurableKitProductAttributeFormSetCreate,
|
||
ConfigurableKitProductAttributeFormSetUpdate
|
||
)
|
||
|
||
|
||
class ConfigurableKitProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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, ManagerOwnerRequiredMixin, 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, ManagerOwnerRequiredMixin, 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'
|
||
)
|
||
|
||
# Доступные комплекты для 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 ConfigurableKitOptionAttribute
|
||
|
||
# Пересоздаём 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():
|
||
# Логирование ошибок 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:
|
||
ConfigurableKitOptionAttribute.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
|
||
|
||
# Сначала удаляем все старые атрибуты
|
||
ConfigurableKitProductAttribute.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 = []
|
||
|
||
# Создаём ConfigurableKitProductAttribute для каждого значения
|
||
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
|
||
|
||
ConfigurableKitProductAttribute.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 ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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'
|
||
)
|
||
|
||
# Доступные комплекты для 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 ConfigurableKitOptionAttribute
|
||
|
||
# Пересоздаём 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():
|
||
# Логирование ошибок 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:
|
||
ConfigurableKitOptionAttribute.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
|
||
|
||
# Сначала удаляем все старые атрибуты
|
||
ConfigurableKitProductAttribute.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 = []
|
||
|
||
# Создаём ConfigurableKitProductAttribute для каждого значения
|
||
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
|
||
|
||
ConfigurableKitProductAttribute.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 ConfigurableKitProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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': 'Вариант установлен как по умолчанию'})
|