Рефакторинг системы вариативных товаров и справочник атрибутов
Основные изменения: - Переименование ConfigurableKitProduct → ConfigurableProduct - Добавлена поддержка Product как варианта (не только ProductKit) - Создан справочник атрибутов (ProductAttribute, ProductAttributeValue) - CRUD для управления атрибутами с inline редактированием значений - Пересозданы миграции с нуля для всех приложений - Добавлена ссылка на атрибуты в навигацию 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,18 +80,30 @@ from .tag_views import (
|
||||
ProductTagDeleteView,
|
||||
)
|
||||
|
||||
# CRUD представления для ConfigurableKitProduct
|
||||
# CRUD представления для ConfigurableProduct
|
||||
from .configurablekit_views import (
|
||||
ConfigurableKitProductListView,
|
||||
ConfigurableKitProductCreateView,
|
||||
ConfigurableKitProductDetailView,
|
||||
ConfigurableKitProductUpdateView,
|
||||
ConfigurableKitProductDeleteView,
|
||||
ConfigurableProductListView,
|
||||
ConfigurableProductCreateView,
|
||||
ConfigurableProductDetailView,
|
||||
ConfigurableProductUpdateView,
|
||||
ConfigurableProductDeleteView,
|
||||
add_option_to_configurable,
|
||||
remove_option_from_configurable,
|
||||
set_option_as_default,
|
||||
)
|
||||
|
||||
# CRUD представления для ProductAttribute (справочник атрибутов)
|
||||
from .attribute_views import (
|
||||
ProductAttributeListView,
|
||||
ProductAttributeCreateView,
|
||||
ProductAttributeDetailView,
|
||||
ProductAttributeUpdateView,
|
||||
ProductAttributeDeleteView,
|
||||
create_attribute_api,
|
||||
add_attribute_value_api,
|
||||
delete_attribute_value_api,
|
||||
)
|
||||
|
||||
# API представления
|
||||
from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api, create_tag_api
|
||||
|
||||
@@ -162,16 +174,26 @@ __all__ = [
|
||||
'ProductTagUpdateView',
|
||||
'ProductTagDeleteView',
|
||||
|
||||
# ConfigurableKitProduct CRUD
|
||||
'ConfigurableKitProductListView',
|
||||
'ConfigurableKitProductCreateView',
|
||||
'ConfigurableKitProductDetailView',
|
||||
'ConfigurableKitProductUpdateView',
|
||||
'ConfigurableKitProductDeleteView',
|
||||
# ConfigurableProduct CRUD
|
||||
'ConfigurableProductListView',
|
||||
'ConfigurableProductCreateView',
|
||||
'ConfigurableProductDetailView',
|
||||
'ConfigurableProductUpdateView',
|
||||
'ConfigurableProductDeleteView',
|
||||
'add_option_to_configurable',
|
||||
'remove_option_from_configurable',
|
||||
'set_option_as_default',
|
||||
|
||||
# ProductAttribute CRUD
|
||||
'ProductAttributeListView',
|
||||
'ProductAttributeCreateView',
|
||||
'ProductAttributeDetailView',
|
||||
'ProductAttributeUpdateView',
|
||||
'ProductAttributeDeleteView',
|
||||
'create_attribute_api',
|
||||
'add_attribute_value_api',
|
||||
'delete_attribute_value_api',
|
||||
|
||||
# API
|
||||
'search_products_and_variants',
|
||||
'validate_kit_cost',
|
||||
|
||||
247
myproject/products/views/attribute_views.py
Normal file
247
myproject/products/views/attribute_views.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
CRUD представления для справочника атрибутов товаров (ProductAttribute, ProductAttributeValue).
|
||||
"""
|
||||
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, Count
|
||||
from django.db import IntegrityError
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib.auth.decorators import login_required
|
||||
import json
|
||||
|
||||
from ..models import ProductAttribute, ProductAttributeValue
|
||||
from ..forms import ProductAttributeForm, ProductAttributeValueFormSet
|
||||
|
||||
|
||||
class ProductAttributeListView(LoginRequiredMixin, ListView):
|
||||
"""Список всех атрибутов с поиском"""
|
||||
model = ProductAttribute
|
||||
template_name = 'products/attribute_list.html'
|
||||
context_object_name = 'attributes'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Аннотируем количество значений для каждого атрибута
|
||||
queryset = queryset.annotate(
|
||||
num_values=Count('values', distinct=True)
|
||||
)
|
||||
|
||||
# Поиск по названию и slug
|
||||
search_query = self.request.GET.get('search')
|
||||
if search_query:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(slug__icontains=search_query)
|
||||
)
|
||||
|
||||
return queryset.order_by('position', 'name')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
return context
|
||||
|
||||
|
||||
class ProductAttributeDetailView(LoginRequiredMixin, DetailView):
|
||||
"""Детальная информация об атрибуте с его значениями"""
|
||||
model = ProductAttribute
|
||||
template_name = 'products/attribute_detail.html'
|
||||
context_object_name = 'attribute'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
attribute = self.get_object()
|
||||
|
||||
# Получаем все значения атрибута
|
||||
context['values'] = attribute.values.all().order_by('position', 'value')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ProductAttributeCreateView(LoginRequiredMixin, CreateView):
|
||||
"""Создание нового атрибута с inline значениями"""
|
||||
model = ProductAttribute
|
||||
form_class = ProductAttributeForm
|
||||
template_name = 'products/attribute_form.html'
|
||||
success_url = reverse_lazy('products:attribute-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context['value_formset'] = ProductAttributeValueFormSet(self.request.POST, instance=self.object)
|
||||
else:
|
||||
context['value_formset'] = ProductAttributeValueFormSet(instance=self.object)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
context = self.get_context_data()
|
||||
value_formset = context['value_formset']
|
||||
|
||||
try:
|
||||
self.object = form.save()
|
||||
|
||||
if value_formset.is_valid():
|
||||
value_formset.instance = self.object
|
||||
value_formset.save()
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
messages.success(self.request, f'Атрибут "{self.object.name}" успешно создан.')
|
||||
return super().form_valid(form)
|
||||
|
||||
except IntegrityError as e:
|
||||
error_msg = str(e).lower()
|
||||
if 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: атрибут с таким названием уже существует.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении атрибута. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ProductAttributeUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""Редактирование существующего атрибута с inline значениями"""
|
||||
model = ProductAttribute
|
||||
form_class = ProductAttributeForm
|
||||
template_name = 'products/attribute_form.html'
|
||||
success_url = reverse_lazy('products:attribute-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context['value_formset'] = ProductAttributeValueFormSet(self.request.POST, instance=self.object)
|
||||
else:
|
||||
context['value_formset'] = ProductAttributeValueFormSet(instance=self.object)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
context = self.get_context_data()
|
||||
value_formset = context['value_formset']
|
||||
|
||||
try:
|
||||
self.object = form.save()
|
||||
|
||||
if value_formset.is_valid():
|
||||
value_formset.save()
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
messages.success(self.request, f'Атрибут "{self.object.name}" успешно обновлен.')
|
||||
return super().form_valid(form)
|
||||
|
||||
except IntegrityError as e:
|
||||
error_msg = str(e).lower()
|
||||
if 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: атрибут с таким названием уже существует.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении атрибута. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ProductAttributeDeleteView(LoginRequiredMixin, DeleteView):
|
||||
"""Удаление атрибута с подтверждением"""
|
||||
model = ProductAttribute
|
||||
template_name = 'products/attribute_confirm_delete.html'
|
||||
success_url = reverse_lazy('products:attribute-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
attribute = self.get_object()
|
||||
|
||||
# Количество значений
|
||||
context['values_count'] = attribute.values.count()
|
||||
|
||||
return context
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
attribute = self.get_object()
|
||||
attribute_name = attribute.name
|
||||
response = super().delete(request, *args, **kwargs)
|
||||
messages.success(request, f'Атрибут "{attribute_name}" успешно удален.')
|
||||
return response
|
||||
|
||||
|
||||
# API endpoints
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def create_attribute_api(request):
|
||||
"""API для быстрого создания атрибута"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
if not name:
|
||||
return JsonResponse({'success': False, 'error': 'Название обязательно'})
|
||||
|
||||
attribute = ProductAttribute.objects.create(name=name)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'id': attribute.pk,
|
||||
'name': attribute.name,
|
||||
'slug': attribute.slug
|
||||
})
|
||||
except IntegrityError:
|
||||
return JsonResponse({'success': False, 'error': 'Атрибут с таким названием уже существует'})
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def add_attribute_value_api(request, pk):
|
||||
"""API для добавления значения к атрибуту"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
value = data.get('value', '').strip()
|
||||
|
||||
if not value:
|
||||
return JsonResponse({'success': False, 'error': 'Значение обязательно'})
|
||||
|
||||
attribute = ProductAttribute.objects.get(pk=pk)
|
||||
attr_value = ProductAttributeValue.objects.create(
|
||||
attribute=attribute,
|
||||
value=value
|
||||
)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'id': attr_value.pk,
|
||||
'value': attr_value.value,
|
||||
'slug': attr_value.slug
|
||||
})
|
||||
except ProductAttribute.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Атрибут не найден'})
|
||||
except IntegrityError:
|
||||
return JsonResponse({'success': False, 'error': 'Такое значение уже существует'})
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_attribute_value_api(request, pk, value_id):
|
||||
"""API для удаления значения атрибута"""
|
||||
try:
|
||||
value = ProductAttributeValue.objects.get(pk=value_id, attribute_id=pk)
|
||||
value.delete()
|
||||
return JsonResponse({'success': True})
|
||||
except ProductAttributeValue.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Значение не найдено'})
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
CRUD представления для вариативных товаров (ConfigurableKitProduct).
|
||||
CRUD представления для вариативных товаров (ConfigurableProduct).
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
@@ -13,18 +13,18 @@ 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 ..models import ConfigurableProduct, ConfigurableProductOption, ProductKit, ConfigurableProductAttribute
|
||||
from ..forms import (
|
||||
ConfigurableKitProductForm,
|
||||
ConfigurableKitOptionFormSetCreate,
|
||||
ConfigurableKitOptionFormSetUpdate,
|
||||
ConfigurableKitProductAttributeFormSetCreate,
|
||||
ConfigurableKitProductAttributeFormSetUpdate
|
||||
ConfigurableProductForm,
|
||||
ConfigurableProductOptionFormSetCreate,
|
||||
ConfigurableProductOptionFormSetUpdate,
|
||||
ConfigurableProductAttributeFormSetCreate,
|
||||
ConfigurableProductAttributeFormSetUpdate
|
||||
)
|
||||
|
||||
|
||||
class ConfigurableKitProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
|
||||
model = ConfigurableKitProduct
|
||||
class ConfigurableProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
|
||||
model = ConfigurableProduct
|
||||
template_name = 'products/configurablekit_list.html'
|
||||
context_object_name = 'configurable_kits'
|
||||
paginate_by = 20
|
||||
@@ -33,7 +33,7 @@ class ConfigurableKitProductListView(LoginRequiredMixin, ManagerOwnerRequiredMix
|
||||
queryset = super().get_queryset().prefetch_related(
|
||||
Prefetch(
|
||||
'options',
|
||||
queryset=ConfigurableKitOption.objects.select_related('kit')
|
||||
queryset=ConfigurableProductOption.objects.select_related('kit')
|
||||
)
|
||||
)
|
||||
|
||||
@@ -80,8 +80,8 @@ class ConfigurableKitProductListView(LoginRequiredMixin, ManagerOwnerRequiredMix
|
||||
return context
|
||||
|
||||
|
||||
class ConfigurableKitProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
|
||||
model = ConfigurableKitProduct
|
||||
class ConfigurableProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
|
||||
model = ConfigurableProduct
|
||||
template_name = 'products/configurablekit_detail.html'
|
||||
context_object_name = 'configurable_kit'
|
||||
|
||||
@@ -89,7 +89,7 @@ class ConfigurableKitProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
return super().get_queryset().prefetch_related(
|
||||
Prefetch(
|
||||
'options',
|
||||
queryset=ConfigurableKitOption.objects.select_related('kit').order_by('id')
|
||||
queryset=ConfigurableProductOption.objects.select_related('kit').order_by('id')
|
||||
),
|
||||
'parent_attributes'
|
||||
)
|
||||
@@ -104,9 +104,9 @@ class ConfigurableKitProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
return context
|
||||
|
||||
|
||||
class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
|
||||
model = ConfigurableKitProduct
|
||||
form_class = ConfigurableKitProductForm
|
||||
class ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
|
||||
model = ConfigurableProduct
|
||||
form_class = ConfigurableProductForm
|
||||
template_name = 'products/configurablekit_form.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -116,12 +116,12 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
if 'option_formset' in kwargs:
|
||||
context['option_formset'] = kwargs['option_formset']
|
||||
elif self.request.POST:
|
||||
context['option_formset'] = ConfigurableKitOptionFormSetCreate(
|
||||
context['option_formset'] = ConfigurableProductOptionFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='options'
|
||||
)
|
||||
else:
|
||||
context['option_formset'] = ConfigurableKitOptionFormSetCreate(
|
||||
context['option_formset'] = ConfigurableProductOptionFormSetCreate(
|
||||
prefix='options'
|
||||
)
|
||||
|
||||
@@ -129,12 +129,12 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
if 'attribute_formset' in kwargs:
|
||||
context['attribute_formset'] = kwargs['attribute_formset']
|
||||
elif self.request.POST:
|
||||
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetCreate(
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='attributes'
|
||||
)
|
||||
else:
|
||||
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetCreate(
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetCreate(
|
||||
prefix='attributes'
|
||||
)
|
||||
|
||||
@@ -147,14 +147,14 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
from products.models.kits import ConfigurableKitOptionAttribute
|
||||
from products.models.kits import ConfigurableProductOptionAttribute
|
||||
|
||||
# Пересоздаём formsets с POST данными
|
||||
option_formset = ConfigurableKitOptionFormSetCreate(
|
||||
option_formset = ConfigurableProductOptionFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='options'
|
||||
)
|
||||
attribute_formset = ConfigurableKitProductAttributeFormSetCreate(
|
||||
attribute_formset = ConfigurableProductAttributeFormSetCreate(
|
||||
self.request.POST,
|
||||
prefix='attributes'
|
||||
)
|
||||
@@ -212,7 +212,7 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
# Сохраняем выбранные атрибуты для этого варианта
|
||||
for field_name, field_value in option_form.cleaned_data.items():
|
||||
if field_name.startswith('attribute_') and field_value:
|
||||
ConfigurableKitOptionAttribute.objects.create(
|
||||
ConfigurableProductOptionAttribute.objects.create(
|
||||
option=option,
|
||||
attribute=field_value
|
||||
)
|
||||
@@ -250,7 +250,7 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
from products.models.kits import ProductKit
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableKitProductAttribute.objects.filter(parent=self.object).delete()
|
||||
ConfigurableProductAttribute.objects.filter(parent=self.object).delete()
|
||||
|
||||
# Получаем количество карточек параметров
|
||||
total_forms_str = self.request.POST.get('attributes-TOTAL_FORMS', '0')
|
||||
@@ -293,7 +293,7 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
|
||||
# Создаём ConfigurableKitProductAttribute для каждого значения
|
||||
# Создаём ConfigurableProductAttribute для каждого значения
|
||||
for value_idx, value in enumerate(values):
|
||||
if value and value.strip():
|
||||
# Получаем соответствующий ID комплекта
|
||||
@@ -317,7 +317,7 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
# Комплект не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableKitProductAttribute.objects.create(**create_kwargs)
|
||||
ConfigurableProductAttribute.objects.create(**create_kwargs)
|
||||
|
||||
def _validate_variant_kits(self, option_formset):
|
||||
"""
|
||||
@@ -376,9 +376,9 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
|
||||
model = ConfigurableKitProduct
|
||||
form_class = ConfigurableKitProductForm
|
||||
class ConfigurableProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
|
||||
model = ConfigurableProduct
|
||||
form_class = ConfigurableProductForm
|
||||
template_name = 'products/configurablekit_form.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -388,13 +388,13 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
if 'option_formset' in kwargs:
|
||||
context['option_formset'] = kwargs['option_formset']
|
||||
elif self.request.POST:
|
||||
context['option_formset'] = ConfigurableKitOptionFormSetUpdate(
|
||||
context['option_formset'] = ConfigurableProductOptionFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='options'
|
||||
)
|
||||
else:
|
||||
context['option_formset'] = ConfigurableKitOptionFormSetUpdate(
|
||||
context['option_formset'] = ConfigurableProductOptionFormSetUpdate(
|
||||
instance=self.object,
|
||||
prefix='options'
|
||||
)
|
||||
@@ -403,13 +403,13 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
if 'attribute_formset' in kwargs:
|
||||
context['attribute_formset'] = kwargs['attribute_formset']
|
||||
elif self.request.POST:
|
||||
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetUpdate(
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='attributes'
|
||||
)
|
||||
else:
|
||||
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetUpdate(
|
||||
context['attribute_formset'] = ConfigurableProductAttributeFormSetUpdate(
|
||||
instance=self.object,
|
||||
prefix='attributes'
|
||||
)
|
||||
@@ -423,15 +423,15 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
from products.models.kits import ConfigurableKitOptionAttribute
|
||||
from products.models.kits import ConfigurableProductOptionAttribute
|
||||
|
||||
# Пересоздаём formsets с POST данными
|
||||
option_formset = ConfigurableKitOptionFormSetUpdate(
|
||||
option_formset = ConfigurableProductOptionFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='options'
|
||||
)
|
||||
attribute_formset = ConfigurableKitProductAttributeFormSetUpdate(
|
||||
attribute_formset = ConfigurableProductAttributeFormSetUpdate(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='attributes'
|
||||
@@ -489,7 +489,7 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
# Сохраняем выбранные атрибуты для этого варианта
|
||||
for field_name, field_value in option_form.cleaned_data.items():
|
||||
if field_name.startswith('attribute_') and field_value:
|
||||
ConfigurableKitOptionAttribute.objects.create(
|
||||
ConfigurableProductOptionAttribute.objects.create(
|
||||
option=option,
|
||||
attribute=field_value
|
||||
)
|
||||
@@ -527,7 +527,7 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
from products.models.kits import ProductKit
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableKitProductAttribute.objects.filter(parent=self.object).delete()
|
||||
ConfigurableProductAttribute.objects.filter(parent=self.object).delete()
|
||||
|
||||
# Получаем количество карточек параметров
|
||||
total_forms_str = self.request.POST.get('attributes-TOTAL_FORMS', '0')
|
||||
@@ -570,7 +570,7 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
|
||||
# Создаём ConfigurableKitProductAttribute для каждого значения
|
||||
# Создаём ConfigurableProductAttribute для каждого значения
|
||||
for value_idx, value in enumerate(values):
|
||||
if value and value.strip():
|
||||
# Получаем соответствующий ID комплекта
|
||||
@@ -594,7 +594,7 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
# Комплект не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableKitProductAttribute.objects.create(**create_kwargs)
|
||||
ConfigurableProductAttribute.objects.create(**create_kwargs)
|
||||
|
||||
def _validate_variant_kits(self, option_formset):
|
||||
"""
|
||||
@@ -653,8 +653,8 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredM
|
||||
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class ConfigurableKitProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DeleteView):
|
||||
model = ConfigurableKitProduct
|
||||
class ConfigurableProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DeleteView):
|
||||
model = ConfigurableProduct
|
||||
template_name = 'products/configurablekit_confirm_delete.html'
|
||||
success_url = reverse_lazy('products:configurablekit-list')
|
||||
|
||||
@@ -671,7 +671,7 @@ def add_option_to_configurable(request, pk):
|
||||
"""
|
||||
Добавить вариант (комплект) к вариативному товару.
|
||||
"""
|
||||
configurable = get_object_or_404(ConfigurableKitProduct, pk=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'
|
||||
@@ -685,15 +685,15 @@ def add_option_to_configurable(request, pk):
|
||||
return JsonResponse({'success': False, 'error': 'Комплект не найден'}, status=404)
|
||||
|
||||
# Проверяем, не добавлен ли уже этот комплект
|
||||
if ConfigurableKitOption.objects.filter(parent=configurable, kit=kit).exists():
|
||||
if ConfigurableProductOption.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)
|
||||
ConfigurableProductOption.objects.filter(parent=configurable, is_default=True).update(is_default=False)
|
||||
|
||||
# Создаём вариант
|
||||
option = ConfigurableKitOption.objects.create(
|
||||
option = ConfigurableProductOption.objects.create(
|
||||
parent=configurable,
|
||||
kit=kit,
|
||||
attributes=attributes,
|
||||
@@ -720,8 +720,8 @@ 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)
|
||||
configurable = get_object_or_404(ConfigurableProduct, pk=pk)
|
||||
option = get_object_or_404(ConfigurableProductOption, pk=option_id, parent=configurable)
|
||||
|
||||
option.delete()
|
||||
|
||||
@@ -734,11 +734,11 @@ 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)
|
||||
configurable = get_object_or_404(ConfigurableProduct, pk=pk)
|
||||
option = get_object_or_404(ConfigurableProductOption, pk=option_id, parent=configurable)
|
||||
|
||||
# Снимаем флаг со всех других
|
||||
ConfigurableKitOption.objects.filter(parent=configurable).update(is_default=False)
|
||||
ConfigurableProductOption.objects.filter(parent=configurable).update(is_default=False)
|
||||
|
||||
# Устанавливаем текущий
|
||||
option.is_default = True
|
||||
|
||||
Reference in New Issue
Block a user