Добавлен 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:
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user