# -*- coding: utf-8 -*- 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.views.decorators.http import require_http_methods from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError from django.db import models from decimal import Decimal from .models import Order, OrderItem, Address, OrderStatus from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentFormSet from .filters import OrderFilter from .services import DraftOrderService from .services.address_service import AddressService import json def order_list(request): """ Список всех заказов с фильтрацией и поиском Использует django-filter для фильтрации данных """ # Базовый queryset с оптимизацией запросов orders = Order.objects.select_related( 'customer', 'delivery_address', 'pickup_warehouse' ).all() # Применяем фильтры через django-filter order_filter = OrderFilter(request.GET, queryset=orders) # Сортировка filtered_orders = order_filter.qs.order_by('-created_at') # Пагинация paginator = Paginator(filtered_orders, 25) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'filter': order_filter, 'page_obj': page_obj, 'status_choices': OrderStatus.objects.all().order_by('order'), } return render(request, 'orders/order_list.html', context) def order_detail(request, order_number): """Детальная информация о заказе""" order = get_object_or_404( Order.objects.select_related('customer', 'delivery_address', 'pickup_warehouse', 'modified_by') .prefetch_related('items__product', 'items__product_kit', 'payments__created_by'), order_number=order_number ) context = { 'order': order, } return render(request, 'orders/order_detail.html', context) def order_create(request): """Создание нового заказа""" if request.method == 'POST': form = OrderForm(request.POST) formset = OrderItemFormSet(request.POST) payment_formset = PaymentFormSet(request.POST) if form.is_valid() and formset.is_valid() and payment_formset.is_valid(): order = form.save(commit=False) # Обрабатываем адрес доставки if order.is_delivery: address = AddressService.process_address_from_form(order, form.cleaned_data) if address: # Если адрес не существует в БД, сохраняем его if not address.pk: address.save() order.delivery_address = address # Если нажата кнопка "Сохранить как черновик", создаем черновик if 'save_as_draft' in request.POST: from .services.order_status_service import OrderStatusService order.status = OrderStatusService.get_draft_status() order.modified_by = request.user order.save() # Сохраняем позиции заказа formset.instance = order formset.save() # Сохраняем платежи payment_formset.instance = order payment_formset.save() # Пересчитываем итоговую сумму order.calculate_total() order.save() if order.is_draft(): messages.success(request, f'Черновик #{order.order_number} успешно создан!') else: messages.success(request, f'Заказ #{order.order_number} успешно создан!') return redirect('orders:order-detail', order_number=order.order_number) else: messages.error(request, 'Пожалуйста, исправьте ошибки в форме.') else: # Предзаполнение клиента из GET параметра initial_data = {} preselected_customer = None customer_id = request.GET.get('customer') if customer_id: try: from customers.models import Customer preselected_customer = Customer.objects.get(pk=customer_id) initial_data['customer'] = preselected_customer.pk except (Customer.DoesNotExist, ValueError): pass form = OrderForm(initial=initial_data) formset = OrderItemFormSet() payment_formset = PaymentFormSet() context = { 'form': form, 'formset': formset, 'payment_formset': payment_formset, 'preselected_customer': preselected_customer, 'title': 'Создание заказа', 'button_text': 'Создать заказ', } return render(request, 'orders/order_form.html', context) def order_update(request, order_number): """Редактирование заказа""" order = get_object_or_404(Order, order_number=order_number) if request.method == 'POST': form = OrderForm(request.POST, instance=order) formset = OrderItemFormSet(request.POST, instance=order) payment_formset = PaymentFormSet(request.POST, instance=order) if form.is_valid() and formset.is_valid() and payment_formset.is_valid(): order = form.save(commit=False) # Если черновик финализируется if 'finalize_draft' in request.POST and order.is_draft(): try: order = DraftOrderService.finalize_draft(order.pk, request.user) messages.success(request, f'Черновик #{order.order_number} успешно завершен и переведен в статус "Новый"!') return redirect('orders:order-detail', order_number=order.order_number) except ValidationError as e: messages.error(request, f'Ошибка финализации: {str(e)}') form = OrderForm(instance=order) formset = OrderItemFormSet(instance=order) payment_formset = PaymentFormSet(instance=order) else: # Обрабатываем адрес доставки if order.is_delivery: address = AddressService.process_address_from_form(order, form.cleaned_data) if address: # Если адрес не существует в БД, сохраняем его if not address.pk: address.save() order.delivery_address = address else: # Если режим "без адреса", удаляем существующий адрес if order.delivery_address: old_address = order.delivery_address order.delivery_address = None # Удаляем старый адрес, если он больше не используется if old_address and not old_address.order: old_address.delete() else: # Если не доставка, удаляем адрес если он был if order.delivery_address: old_address = order.delivery_address order.delivery_address = None # Удаляем старый адрес if old_address and not old_address.order: old_address.delete() order.modified_by = request.user order.save() formset.save() # Сохраняем платежи payment_formset.save() # Пересчитываем итоговую сумму order.calculate_total() order.save() if order.is_draft(): messages.success(request, f'Черновик #{order.order_number} успешно обновлен!') else: messages.success(request, f'Заказ #{order.order_number} успешно обновлен!') return redirect('orders:order-detail', order_number=order.order_number) else: # Логируем ошибки для отладки print("\n=== ОШИБКИ ВАЛИДАЦИИ ФОРМЫ ===") if not form.is_valid(): print(f"OrderForm errors: {form.errors}") if not formset.is_valid(): print(f"OrderItemFormSet errors: {formset.errors}") print(f"OrderItemFormSet non_form_errors: {formset.non_form_errors()}") for i, item_form in enumerate(formset): if item_form.errors: print(f" Item form {i} errors: {item_form.errors}") if not payment_formset.is_valid(): print(f"PaymentFormSet errors: {payment_formset.errors}") print(f"PaymentFormSet non_form_errors: {payment_formset.non_form_errors()}") print("=== КОНЕЦ ОШИБОК ===\n") messages.error(request, 'Пожалуйста, исправьте ошибки в форме.') else: form = OrderForm(instance=order) formset = OrderItemFormSet(instance=order) payment_formset = PaymentFormSet(instance=order) context = { 'form': form, 'formset': formset, 'payment_formset': payment_formset, 'order': order, 'title': f'Редактирование {"черновика" if order.is_draft() else "заказа"} #{order.order_number}', 'button_text': 'Сохранить изменения', 'is_draft': order.is_draft(), } return render(request, 'orders/order_form.html', context) def order_delete(request, order_number): """Удаление заказа с подтверждением""" order = get_object_or_404(Order, order_number=order_number) if request.method == 'POST': order_number = order.order_number order.delete() messages.success(request, f'Заказ #{order_number} успешно удален.') return redirect('orders:order-list') context = { 'order': order, } return render(request, 'orders/order_confirm_delete.html', context) # === AJAX ENDPOINTS === @require_http_methods(["POST"]) @login_required def autosave_draft_order(request, order_number): """ AJAX endpoint для автосохранения черновика заказа. Принимает JSON с данными формы и обновляет черновик. Возвращает статус сохранения и время последнего сохранения. Пример запроса: { "customer": 1, "is_delivery": true, "delivery_address": 5, "delivery_date": "2024-01-15", "special_instructions": "Позвонить за час", "items": [ {"product_id": 10, "quantity": "2", "price": "500"}, {"product_kit_id": 5, "quantity": "1", "price": "1500"} ] } Ответ при успехе: { "success": true, "last_saved": "2024-01-10T15:30:45.123456", "order_id": 123, "order_number": "ORD-000123" } """ try: data = json.loads(request.body) # Проверяем существование заказа try: order = Order.objects.get(order_number=order_number) except Order.DoesNotExist: return JsonResponse({ 'success': False, 'error': 'Заказ не найден' }, status=404) # Обновляем основные поля заказа из DraftOrderService (БЕЗ товаров) # Товары обрабатываем отдельно ниже order_fields_only = {k: v for k, v in data.items() if k not in ['items', 'payments']} order = DraftOrderService.update_draft( order_id=order.pk, user=request.user, data=order_fields_only ) # Обрабатываем позиции заказа, если они переданы if 'items' in data: from decimal import Decimal, InvalidOperation # Получаем ID товаров, которые нужно удалить deleted_item_ids = data.get('deleted_item_ids', []) if deleted_item_ids: order.items.filter(pk__in=deleted_item_ids).delete() # Обрабатываем каждый товар for item_data in data['items']: item_id = item_data.get('id') # ID существующего товара (если есть) product_id = item_data.get('product_id') product_kit_id = item_data.get('product_kit_id') quantity = item_data.get('quantity') price_raw = item_data.get('price') # Преобразуем цену try: price = Decimal(str(price_raw).replace(',', '.')) if price_raw else None except (ValueError, InvalidOperation): price = None # Если есть ID - обновляем существующий товар if item_id: try: item = order.items.get(pk=item_id) # Обновляем поля if product_id: from products.models import Product item.product = Product.objects.get(pk=product_id) item.product_kit = None elif product_kit_id: from products.models import ProductKit item.product_kit = ProductKit.objects.get(pk=product_kit_id) item.product = None if quantity: item.quantity = quantity if price is not None: item.price = price item.save() except OrderItem.DoesNotExist: # Если товар не найден, создаем новый item_id = None # Если нет ID - создаем новый товар if not item_id: if product_id: DraftOrderService.add_item_to_draft( order_id=order.pk, product_id=product_id, quantity=quantity, price=price ) elif product_kit_id: DraftOrderService.add_item_to_draft( order_id=order.pk, product_kit_id=product_kit_id, quantity=quantity, price=price ) # НЕ ОБРАБАТЫВАЕМ ПЛАТЕЖИ В АВТОСОХРАНЕНИИ # Платежи обрабатываются только при ручном сохранении формы # Пересчитываем итоговую сумму заказа и обновляем статус оплаты order.calculate_total() order.update_payment_status() order.save() return JsonResponse({ 'success': True, 'last_saved': order.last_autosave_at.isoformat() if order.last_autosave_at else None, 'order_id': order.pk, 'order_number': order.order_number }) except ValidationError as e: return JsonResponse({ 'success': False, 'error': str(e) }, status=400) 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) @require_http_methods(["POST"]) @login_required def create_draft_from_form(request): """ AJAX endpoint для создания черновика заказа из формы создания. Используется для автоматического создания черновика при первом изменении формы. После создания возвращает ID черновика для перенаправления. Пример запроса: { "customer": 1, "is_delivery": true, "delivery_date": "2024-01-15" } Ответ при успехе: { "success": true, "order_id": 123, "order_number": "ORD-000123", "redirect_url": "/orders/123/edit/" } """ try: data = json.loads(request.body) # Получаем обязательное поле - клиента customer_id = data.get('customer') if not customer_id: return JsonResponse({ 'success': False, 'error': 'Необходимо выбрать клиента' }, status=400) from customers.models import Customer try: customer = Customer.objects.get(pk=customer_id) except Customer.DoesNotExist: return JsonResponse({ 'success': False, 'error': 'Клиент не найден' }, status=404) # Создаем черновик через DraftOrderService order = DraftOrderService.create_draft( user=request.user, customer=customer, data=data ) # Обрабатываем позиции заказа, если они переданы if 'items' in data: for item_data in data['items']: product_id = item_data.get('product_id') product_kit_id = item_data.get('product_kit_id') quantity = item_data.get('quantity') price = item_data.get('price') if product_id: DraftOrderService.add_item_to_draft( order_id=order.pk, product_id=product_id, quantity=quantity, price=price ) elif product_kit_id: DraftOrderService.add_item_to_draft( order_id=order.pk, product_kit_id=product_kit_id, quantity=quantity, price=price ) return JsonResponse({ 'success': True, 'order_id': order.pk, 'order_number': order.order_number, 'redirect_url': f'/orders/{order.order_number}/edit/' }) except ValidationError as e: return JsonResponse({ 'success': False, 'error': str(e) }, status=400) 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) @require_http_methods(["GET"]) @login_required def get_customer_address_history(request): """ AJAX endpoint для получения истории адресов клиента. GET параметры: - customer_id: ID клиента Возвращает JSON со списком адресов из истории заказов клиента. """ try: customer_id = request.GET.get('customer_id') if not customer_id: return JsonResponse({ 'success': False, 'error': 'customer_id не указан' }, status=400) from customers.models import Customer try: customer = Customer.objects.get(pk=customer_id) except Customer.DoesNotExist: return JsonResponse({ 'success': False, 'error': 'Клиент не найден' }, status=404) # Получаем адреса из истории заказов addresses = AddressService.get_customer_address_history(customer) # Форматируем для отправки клиенту addresses_data = [ { 'id': addr.id, 'display': AddressService.format_address_for_display(addr), 'street': addr.street, 'building_number': addr.building_number, 'apartment_number': addr.apartment_number, 'entrance': addr.entrance, 'floor': addr.floor, 'intercom_code': addr.intercom_code, 'recipient_name': addr.recipient_name, 'recipient_phone': addr.recipient_phone, } for addr in addresses ] return JsonResponse({ 'success': True, 'addresses': addresses_data, 'count': len(addresses_data) }) except Exception as e: return JsonResponse({ 'success': False, 'error': f'Ошибка сервера: {str(e)}' }, status=500) # === УПРАВЛЕНИЕ СТАТУСАМИ ЗАКАЗОВ === @login_required def order_status_list(request): """Список всех статусов заказов""" statuses = OrderStatus.objects.all().order_by('order', 'name') # orders_count уже доступен как property в модели OrderStatus # Не нужно вручную добавлять атрибут context = { 'statuses': statuses, 'title': 'Статусы заказов' } return render(request, 'orders/status_list.html', context) @login_required def order_status_create(request): """Создание нового статуса""" if request.method == 'POST': form = OrderStatusForm(request.POST) if form.is_valid(): status = form.save(commit=False) status.created_by = request.user status.updated_by = request.user # Если не указан порядок - делаем его последним if not status.order: max_order = OrderStatus.objects.aggregate(models.Max('order'))['order__max'] or 0 status.order = max_order + 10 status.save() messages.success(request, f'Статус "{status.name}" успешно создан') return redirect('orders:status_list') else: form = OrderStatusForm() context = { 'form': form, 'title': 'Создать новый статус', 'button_text': 'Создать' } return render(request, 'orders/status_form.html', context) @login_required def order_status_update(request, pk): """Редактирование статуса""" status = get_object_or_404(OrderStatus, pk=pk) if request.method == 'POST': form = OrderStatusForm(request.POST, instance=status) if form.is_valid(): status = form.save(commit=False) status.updated_by = request.user status.save() messages.success(request, f'Статус "{status.name}" успешно обновлен') return redirect('orders:status_list') else: form = OrderStatusForm(instance=status) context = { 'form': form, 'status': status, 'title': f'Редактировать статус: {status.name}', 'button_text': 'Сохранить', 'is_system': status.is_system } return render(request, 'orders/status_form.html', context) @login_required def order_status_delete(request, pk): """Удаление статуса""" status = get_object_or_404(OrderStatus, pk=pk) if status.is_system: messages.error(request, f'Нельзя удалить системный статус "{status.name}"') return redirect('orders:status_list') if request.method == 'POST': # Проверяем, что статус не используется в заказах orders_count = Order.objects.filter(status=status).count() if orders_count > 0: messages.error( request, f'Невозможно удалить статус. Есть {orders_count} заказов с этим статусом.' ) return redirect('orders:status_list') status_name = status.name status.delete() messages.success(request, f'Статус "{status_name}" успешно удален') return redirect('orders:status_list') # Информация для подтверждения удаления orders_count = Order.objects.filter(status=status).count() context = { 'status': status, 'orders_count': orders_count, 'title': 'Удалить статус' } return render(request, 'orders/status_confirm_delete.html', context) # === ВРЕМЕННЫЕ КОМПЛЕКТЫ === # УДАЛЕНО: Логика создания временных комплектов перенесена в products.services.kit_service # Используйте API endpoint: products:api-temporary-kit-create # === КОШЕЛЁК КЛИЕНТА === @login_required def apply_wallet_payment(request, order_number): """ Применение оплаты из кошелька клиента к заказу. Вызывается через POST-запрос с суммой для списания. """ if request.method != 'POST': return redirect('orders:order-detail', order_number=order_number) order = get_object_or_404(Order, order_number=order_number) # Получаем запрашиваемую сумму из формы try: raw_amount = request.POST.get('wallet_amount', '0') amount = Decimal(str(raw_amount).replace(',', '.')) except (ValueError, TypeError, ArithmeticError): messages.error(request, 'Некорректная сумма для списания из кошелька.') return redirect('orders:order-detail', pk=pk) # Вызываем сервис для оплаты из кошелька try: from customers.services.wallet_service import WalletService paid_amount = WalletService.pay_with_wallet(order, amount, request.user) if paid_amount and paid_amount > 0: messages.success( request, f'Из кошелька клиента списано {paid_amount} руб. для оплаты заказа #{order.order_number}.' ) else: messages.warning( request, 'Не удалось списать средства из кошелька. Проверьте баланс и сумму заказа.' ) except ValueError as e: messages.error(request, str(e)) except Exception as e: messages.error(request, f'Ошибка при оплате из кошелька: {str(e)}') return redirect('orders:order-detail', order_number=order.order_number) @require_http_methods(["POST"]) @login_required def set_order_status(request, order_number): """ Update order status via AJAX. Accepts POST with 'status_id' (can be empty to clear). Returns JSON with the resulting status info. """ try: order = get_object_or_404(Order, order_number=order_number) status_id = request.POST.get('status_id', '').strip() # Allow clearing status if empty if status_id == '': order.status = None order.modified_by = request.user order.save(update_fields=['status', 'modified_by', 'updated_at']) return JsonResponse({'success': True, 'status': None}) try: status = OrderStatus.objects.get(pk=status_id) except OrderStatus.DoesNotExist: return JsonResponse({'success': False, 'error': 'Status not found'}, status=404) order.status = status order.modified_by = request.user order.save(update_fields=['status', 'modified_by', 'updated_at']) return JsonResponse({ 'success': True, 'status': { 'id': status.pk, 'name': status.label or status.name, 'color': status.color } }) except Exception as e: return JsonResponse({'success': False, 'error': f'Server error: {str(e)}'}, status=500)