Enforce parameter binding requirement for ConfigurableKitProduct variants
Changes: 1. Removed unused attributesMetadata container from configurablekit_form.html - Dead code from old formset-based attribute system - 10 lines of unused HTML and templating removed 2. Enhanced formset validation in BaseConfigurableKitOptionFormSet.clean(): - If product HAS parameters: variants MUST select values for ALL parameters - If product HAS NO parameters: variants MUST NOT be created - Error message guides user to add parameters first Business logic: - ConfigurableKitProduct variants (options) are ALWAYS bound to attribute values - You cannot create orphan variants without parameter selections - Each variant must have a value for every product parameter User experience: - Clear error message if trying to add variant without parameters - Enforces proper product structure: parameters first, then variants 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
44
check_kit_bindings.py
Normal file
44
check_kit_bindings.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Check existing kit bindings in database
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from products.models.kits import ConfigurableKitProduct, ConfigurableKitProductAttribute
|
||||||
|
from django_tenants.utils import tenant_context
|
||||||
|
from tenants.models import Client
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = Client.objects.get(schema_name='grach')
|
||||||
|
except Client.DoesNotExist:
|
||||||
|
print("Tenant 'grach' not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with tenant_context(client):
|
||||||
|
print("=" * 80)
|
||||||
|
print("Current ConfigurableKitProduct items in database:")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
for product in ConfigurableKitProduct.objects.all().order_by('-id')[:5]:
|
||||||
|
attrs_count = product.parent_attributes.count()
|
||||||
|
kit_bound = product.parent_attributes.filter(kit__isnull=False).count()
|
||||||
|
|
||||||
|
print(f"\nID {product.id}: {product.name} (SKU: {product.sku})")
|
||||||
|
print(f" Total attributes: {attrs_count}")
|
||||||
|
print(f" Kit-bound attributes: {kit_bound}")
|
||||||
|
|
||||||
|
if attrs_count > 0:
|
||||||
|
print(" Attribute breakdown:")
|
||||||
|
params = product.parent_attributes.values('name').distinct()
|
||||||
|
for param in params:
|
||||||
|
param_name = param['name']
|
||||||
|
values = product.parent_attributes.filter(name=param_name).values_list('option', 'kit__name')
|
||||||
|
print(f" - {param_name}:")
|
||||||
|
for value, kit_name in values:
|
||||||
|
kit_info = f"Kit: {kit_name}" if kit_name else "(no kit)"
|
||||||
|
print(f" * {value} -> {kit_info}")
|
||||||
27
myproject/check_kit_bindings.py
Normal file
27
myproject/check_kit_bindings.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os, sys, django
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from products.models.kits import ConfigurableKitProduct
|
||||||
|
from django_tenants.utils import tenant_context
|
||||||
|
from tenants.models import Client
|
||||||
|
|
||||||
|
client = Client.objects.get(schema_name='grach')
|
||||||
|
with tenant_context(client):
|
||||||
|
print("=" * 80)
|
||||||
|
print("ConfigurableKitProduct items with kit bindings:")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
for product in ConfigurableKitProduct.objects.all().order_by('-id')[:5]:
|
||||||
|
attrs = product.parent_attributes.all()
|
||||||
|
if attrs.exists():
|
||||||
|
kit_bound = attrs.filter(kit__isnull=False).count()
|
||||||
|
print(f"\nID {product.id}: {product.name}")
|
||||||
|
print(f" Total attrs: {attrs.count()} | Kit-bound: {kit_bound}")
|
||||||
|
for param_name in attrs.values_list('name', flat=True).distinct():
|
||||||
|
vals = attrs.filter(name=param_name)
|
||||||
|
print(f" {param_name}:")
|
||||||
|
for attr in vals:
|
||||||
|
kit = attr.kit.name if attr.kit else "(no kit)"
|
||||||
|
print(f" - {attr.option} -> {kit}")
|
||||||
@@ -722,7 +722,7 @@ class BaseConfigurableKitOptionFormSet(forms.BaseInlineFormSet):
|
|||||||
parent = self.instance
|
parent = self.instance
|
||||||
if parent and parent.pk:
|
if parent and parent.pk:
|
||||||
# Получаем все уникальные названия атрибутов родителя
|
# Получаем все уникальные названия атрибутов родителя
|
||||||
attribute_names = (
|
attribute_names = list(
|
||||||
parent.parent_attributes
|
parent.parent_attributes
|
||||||
.all()
|
.all()
|
||||||
.order_by('position', 'name')
|
.order_by('position', 'name')
|
||||||
@@ -730,6 +730,8 @@ class BaseConfigurableKitOptionFormSet(forms.BaseInlineFormSet):
|
|||||||
.values_list('name', flat=True)
|
.values_list('name', flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Если у товара есть параметры, вариант ОБЯЗАН иметь значения для них
|
||||||
|
if attribute_names:
|
||||||
# Проверяем что каждый атрибут выбран
|
# Проверяем что каждый атрибут выбран
|
||||||
missing_attributes = []
|
missing_attributes = []
|
||||||
for attr_name in attribute_names:
|
for attr_name in attribute_names:
|
||||||
@@ -742,6 +744,11 @@ class BaseConfigurableKitOptionFormSet(forms.BaseInlineFormSet):
|
|||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
f'Вариант {idx + 1}: необходимо заполнить атрибут(ы) {attrs_str}.'
|
f'Вариант {idx + 1}: необходимо заполнить атрибут(ы) {attrs_str}.'
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Если у товара нет параметров, вариант без привязки к параметрам бессмысленен
|
||||||
|
raise forms.ValidationError(
|
||||||
|
f'Вариант {idx + 1}: сначала добавьте параметры товара в разделе "Параметры товара".'
|
||||||
|
)
|
||||||
|
|
||||||
# Проверяем, что не более одного "is_default"
|
# Проверяем, что не более одного "is_default"
|
||||||
if default_count > 1:
|
if default_count > 1:
|
||||||
|
|||||||
@@ -258,17 +258,6 @@ input[name*="DELETE"] {
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Скрытый контейнер с информацией об атрибутах для JavaScript -->
|
|
||||||
<div id="attributesMetadata" style="display: none;">
|
|
||||||
{% for attr in attribute_formset %}
|
|
||||||
{% if attr.cleaned_data or attr.instance.pk %}
|
|
||||||
<div data-attr-name="{{ attr.cleaned_data.name|default:attr.instance.name }}"
|
|
||||||
data-attr-id="{{ attr.instance.id }}">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addOptionBtn">
|
<button type="button" class="btn btn-sm btn-outline-primary" id="addOptionBtn">
|
||||||
<i class="bi bi-plus-circle me-1"></i>Добавить вариант
|
<i class="bi bi-plus-circle me-1"></i>Добавить вариант
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user