From 17b2d706f7278e361c42d12ad0a5b870668cfa44 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 8 Nov 2025 15:09:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20backend=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D0=B2=20=D0=B2=20=D0=B7=D0=B0=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- myproject/orders/forms.py | 77 +++++++++++++++++++++++ myproject/orders/urls.py | 3 + myproject/orders/views.py | 126 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+) diff --git a/myproject/orders/forms.py b/myproject/orders/forms.py index ba8650c..a08464b 100644 --- a/myproject/orders/forms.py +++ b/myproject/orders/forms.py @@ -154,3 +154,80 @@ OrderItemFormSet = inlineformset_factory( min_num=1, # Минимум 1 товар в заказе 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, +) diff --git a/myproject/orders/urls.py b/myproject/orders/urls.py index 31b3442..27db4c9 100644 --- a/myproject/orders/urls.py +++ b/myproject/orders/urls.py @@ -10,4 +10,7 @@ urlpatterns = [ path('/', views.order_detail, name='order-detail'), path('/edit/', views.order_update, name='order-update'), path('/delete/', views.order_delete, name='order-delete'), + + # Временные комплекты + path('temporary-kits/create/', views.create_temporary_kit, name='temporary-kit-create'), ] diff --git a/myproject/orders/views.py b/myproject/orders/views.py index bb95f5f..7e83977 100644 --- a/myproject/orders/views.py +++ b/myproject/orders/views.py @@ -2,9 +2,13 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages 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 .forms import OrderForm, OrderItemFormSet from .filters import OrderFilter +from products.models import ProductKit, KitItem, Product def order_list(request): @@ -138,3 +142,125 @@ def order_delete(request, pk): } 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)