Рефакторинг системы вариативных товаров и справочник атрибутов
Основные изменения: - Переименование 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:
@@ -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