Добавлен backend для создания временных комплектов в заказах

Forms (orders/forms.py):
- TemporaryKitForm: упрощенная форма для временного комплекта (название + описание)
- TemporaryKitItemForm: форма для компонента временного комплекта
- TemporaryKitItemFormSet: formset для управления компонентами

Views (orders/views.py):
- create_temporary_kit: AJAX endpoint для создания временного комплекта
  * Принимает JSON с названием, описанием и списком компонентов
  * Создает комплект с is_temporary=True
  * Связывает с заказом если указан order_id
  * Автоматически пересчитывает цену
  * Возвращает JSON с данными созданного комплекта

URLs (orders/urls.py):
- /orders/temporary-kits/create/ - endpoint для создания

Теперь можно создавать временные комплекты через AJAX запрос.
Следующий шаг - UI в форме заказа.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 15:09:12 +03:00
parent ea4bb5a43b
commit 17b2d706f7
3 changed files with 206 additions and 0 deletions

View File

@@ -154,3 +154,80 @@ OrderItemFormSet = inlineformset_factory(
min_num=1, # Минимум 1 товар в заказе min_num=1, # Минимум 1 товар в заказе
validate_min=True, validate_min=True,
) )
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
class TemporaryKitForm(forms.ModelForm):
"""
Упрощенная форма для создания временного комплекта.
Используется при оформлении заказа для создания букета "на лету".
"""
class Meta:
model = ProductKit
fields = ['name', 'description']
widgets = {
'description': forms.Textarea(attrs={'rows': 2, 'placeholder': 'Краткое описание (опционально)'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Bootstrap классы
for field in self.fields.values():
if isinstance(field.widget, forms.Textarea):
field.widget.attrs.update({'class': 'form-control'})
else:
field.widget.attrs.update({'class': 'form-control'})
# Название обязательно
self.fields['name'].required = True
self.fields['name'].widget.attrs.update({
'placeholder': 'Название временного букета (например: "Букет для Анны")'
})
# Описание опционально
self.fields['description'].required = False
class TemporaryKitItemForm(forms.Form):
"""
Форма для компонента временного комплекта.
Используется в формсете для добавления товаров в букет.
"""
product = forms.IntegerField(required=False, widget=forms.HiddenInput())
quantity = forms.DecimalField(
required=False,
min_value=0.001,
max_digits=10,
decimal_places=3,
widget=forms.NumberInput(attrs={'class': 'form-control', 'min': '0.001', 'step': '1'})
)
def clean(self):
cleaned_data = super().clean()
product_id = cleaned_data.get('product')
quantity = cleaned_data.get('quantity')
# Пустая форма - это нормально (будет удалена)
if not product_id:
return cleaned_data
# Если выбран товар, количество обязательно
if product_id and (not quantity or quantity <= 0):
raise forms.ValidationError('Необходимо указать количество больше 0')
return cleaned_data
# Formset для компонентов временного комплекта
from django.forms import formset_factory
TemporaryKitItemFormSet = formset_factory(
TemporaryKitItemForm,
extra=1, # Одна пустая форма для добавления
can_delete=True,
min_num=1, # Минимум 1 компонент в комплекте
validate_min=True,
)

View File

@@ -10,4 +10,7 @@ urlpatterns = [
path('<int:pk>/', views.order_detail, name='order-detail'), path('<int:pk>/', views.order_detail, name='order-detail'),
path('<int:pk>/edit/', views.order_update, name='order-update'), path('<int:pk>/edit/', views.order_update, name='order-update'),
path('<int:pk>/delete/', views.order_delete, name='order-delete'), path('<int:pk>/delete/', views.order_delete, name='order-delete'),
# Временные комплекты
path('temporary-kits/create/', views.create_temporary_kit, name='temporary-kit-create'),
] ]

View File

@@ -2,9 +2,13 @@
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages from django.contrib import messages
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import JsonResponse
from django.db import transaction
from django.views.decorators.http import require_http_methods
from .models import Order, OrderItem from .models import Order, OrderItem
from .forms import OrderForm, OrderItemFormSet from .forms import OrderForm, OrderItemFormSet
from .filters import OrderFilter from .filters import OrderFilter
from products.models import ProductKit, KitItem, Product
def order_list(request): def order_list(request):
@@ -138,3 +142,125 @@ def order_delete(request, pk):
} }
return render(request, 'orders/order_confirm_delete.html', context) return render(request, 'orders/order_confirm_delete.html', context)
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
@require_http_methods(["POST"])
def create_temporary_kit(request):
"""
AJAX endpoint для создания временного комплекта.
Используется при оформлении заказа для создания букета "на лету".
Принимает JSON:
{
"name": "Букет для Анны",
"description": "Красные розы и белые лилии",
"order_id": 123, // опционально, если заказ уже создан
"components": [
{"product_id": 1, "quantity": "5"},
{"product_id": 2, "quantity": "3"}
]
}
Возвращает JSON:
{
"success": true,
"kit_id": 456,
"kit_name": "Букет для Анны",
"kit_sku": "KIT-000456",
"kit_price": "1500.00",
"message": "Временный комплект создан успешно"
}
"""
import json
from decimal import Decimal
try:
data = json.loads(request.body)
name = data.get('name', '').strip()
description = data.get('description', '').strip()
order_id = data.get('order_id')
components = data.get('components', [])
# Валидация
if not name:
return JsonResponse({
'success': False,
'error': 'Необходимо указать название комплекта'
}, status=400)
if not components or len(components) == 0:
return JsonResponse({
'success': False,
'error': 'Комплект должен содержать хотя бы один компонент'
}, status=400)
# Создаем временный комплект
with transaction.atomic():
# Получаем заказ если указан
order = None
if order_id:
try:
order = Order.objects.get(pk=order_id)
except Order.DoesNotExist:
return JsonResponse({
'success': False,
'error': f'Заказ #{order_id} не найден'
}, status=404)
# Создаем комплект
kit = ProductKit.objects.create(
name=name,
description=description,
is_temporary=True,
is_active=True,
order=order,
price_adjustment_type='none'
)
# Добавляем компоненты
for component in components:
product_id = component.get('product_id')
quantity = component.get('quantity')
if not product_id or not quantity:
continue
try:
product = Product.objects.get(pk=product_id)
KitItem.objects.create(
kit=kit,
product=product,
quantity=Decimal(str(quantity))
)
except Product.DoesNotExist:
# Пропускаем несуществующие товары
continue
except (ValueError, TypeError):
# Пропускаем некорректные количества
continue
# Пересчитываем цену комплекта
kit.recalculate_base_price()
return JsonResponse({
'success': True,
'kit_id': kit.id,
'kit_name': kit.name,
'kit_sku': kit.sku,
'kit_price': str(kit.actual_price),
'message': f'Временный комплект "{kit.name}" создан успешно'
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Некорректный JSON'
}, status=400)
except Exception as e:
return JsonResponse({
'success': False,
'error': f'Ошибка при создании комплекта: {str(e)}'
}, status=500)