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:
2025-11-18 21:46:58 +03:00
parent b1f0d99324
commit 29859503a7
4 changed files with 88 additions and 21 deletions

44
check_kit_bindings.py Normal file
View 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}")

View 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}")

View File

@@ -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,17 +730,24 @@ class BaseConfigurableKitOptionFormSet(forms.BaseInlineFormSet):
.values_list('name', flat=True) .values_list('name', flat=True)
) )
# Проверяем что каждый атрибут выбран # Если у товара есть параметры, вариант ОБЯЗАН иметь значения для них
missing_attributes = [] if attribute_names:
for attr_name in attribute_names: # Проверяем что каждый атрибут выбран
field_name = f'attribute_{attr_name}' missing_attributes = []
if field_name not in form.cleaned_data or not form.cleaned_data[field_name]: for attr_name in attribute_names:
missing_attributes.append(attr_name) field_name = f'attribute_{attr_name}'
if field_name not in form.cleaned_data or not form.cleaned_data[field_name]:
missing_attributes.append(attr_name)
if missing_attributes: if missing_attributes:
attrs_str = ', '.join(f'"{attr}"' for attr in missing_attributes) attrs_str = ', '.join(f'"{attr}"' for attr in missing_attributes)
raise forms.ValidationError(
f'Вариант {idx + 1}: необходимо заполнить атрибут(ы) {attrs_str}.'
)
else:
# Если у товара нет параметров, вариант без привязки к параметрам бессмысленен
raise forms.ValidationError( raise forms.ValidationError(
f'Вариант {idx + 1}: необходимо заполнить атрибут(ы) {attrs_str}.' f'Вариант {idx + 1}: сначала добавьте параметры товара в разделе "Параметры товара".'
) )
# Проверяем, что не более одного "is_default" # Проверяем, что не более одного "is_default"

View File

@@ -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>