# -*- 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, transaction from decimal import Decimal from .models import Order, OrderItem, Address, OrderStatus, Transaction, PaymentMethod from .forms import OrderForm, OrderItemFormSet, OrderItemForm, OrderStatusForm, TransactionForm from .filters import OrderFilter 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', 'transactions__created_by'), order_number=order_number ) context = { 'order': order, } return render(request, 'orders/order_detail.html', context) def order_create(request): """Создание нового заказа""" # Инициализация переменных для контекста preselected_customer = None draft_items = [] if request.method == 'POST': # Логирование POST-данных для отладки print("\n=== POST DATA ===") print(f"items-TOTAL_FORMS: {request.POST.get('items-TOTAL_FORMS')}") print(f"items-INITIAL_FORMS: {request.POST.get('items-INITIAL_FORMS')}") print(f"items-MIN_NUM_FORMS: {request.POST.get('items-MIN_NUM_FORMS')}") print(f"items-MAX_NUM_FORMS: {request.POST.get('items-MAX_NUM_FORMS')}") # Показываем все формы товаров total_forms = int(request.POST.get('items-TOTAL_FORMS', 0)) for i in range(total_forms): product = request.POST.get(f'items-{i}-product', '') kit = request.POST.get(f'items-{i}-product_kit', '') quantity = request.POST.get(f'items-{i}-quantity', '') price = request.POST.get(f'items-{i}-price', '') print(f"\nForm {i}:") print(f" product: {product or '(пусто)'}") print(f" kit: {kit or '(пусто)'}") print(f" quantity: {quantity or '(пусто)'}") print(f" price: {price or '(пусто)'}") print("=== END POST DATA ===\n") form = OrderForm(request.POST) formset = OrderItemFormSet(request.POST) if form.is_valid() and formset.is_valid(): try: with transaction.atomic(): # Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items 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 # Статус берём из формы (в том числе может быть "Черновик") order.modified_by = request.user # Сохраняем заказ в БД (теперь у него есть pk) order.save() # Сохраняем позиции заказа formset.instance = order formset.save() # Пересчитываем стоимость доставки если она не установлена вручную delivery_cost = form.cleaned_data.get('delivery_cost') if not delivery_cost or delivery_cost <= 0: order.reset_delivery_cost() # Пересчитываем итоговую стоимость order.calculate_total() order.update_payment_status() messages.success(request, f'Заказ #{order.order_number} успешно создан!') return redirect('orders:order-detail', order_number=order.order_number) except ValueError as e: # Ошибка в сигналах (например, не удалось создать Sale) # Транзакция откатилась, заказ НЕ создан messages.error(request, f'Ошибка при создании заказа: {e}') else: messages.error(request, 'Пожалуйста, исправьте ошибки в форме.') else: # Предзаполнение клиента из GET параметра initial_data = {} customer_id = request.GET.get('customer') # Проверяем, есть ли черновик из POS draft_token = request.GET.get('draft') if draft_token: from django.core.cache import cache cache_key = f'pos_draft:{draft_token}' draft_data = cache.get(cache_key) if draft_data: # Загружаем клиента из черновика customer_id = draft_data.get('customer_id') draft_items = draft_data.get('items', []) # Удаляем черновик из кэша (одноразовый токен) cache.delete(cache_key) 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 с предзаполненными товарами из черновика if draft_items: from django.forms import inlineformset_factory from products.models import Product, ProductKit # Создаём формсет с нужным количеством extra форм DraftOrderItemFormSet = inlineformset_factory( Order, OrderItem, form=OrderItemForm, extra=len(draft_items), # Создаём столько форм, сколько товаров can_delete=True, min_num=0, validate_min=False, ) formset = DraftOrderItemFormSet() else: formset = OrderItemFormSet() context = { 'form': form, 'formset': formset, 'preselected_customer': preselected_customer, 'draft_items_json': json.dumps(draft_items) if draft_items else '[]', # Передаём JSON товары из черновика 'title': 'Создание заказа', 'button_text': 'Создать заказ', 'is_create_page': True, } return render(request, 'orders/order_form.html', context) def order_update(request, order_number): """Редактирование заказа""" order = get_object_or_404(Order, order_number=order_number) # Пересчитываем amount_paid на основе транзакций (на случай миграции) order.recalculate_amount_paid() if request.method == 'POST': form = OrderForm(request.POST, instance=order) formset = OrderItemFormSet(request.POST, instance=order) if form.is_valid() and formset.is_valid(): try: with transaction.atomic(): 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 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() # Пересчитываем итоговую стоимость order.calculate_total() order.update_payment_status() messages.success(request, f'Заказ #{order.order_number} успешно обновлен!') return redirect('orders:order-detail', order_number=order.order_number) except ValidationError as e: # Ошибка валидации (например, запрет смены статуса для возвращённого заказа) # Транзакция откатилась, заказ НЕ изменился # Показываем сообщение из исключения без служебных элементов error_message = str(e.message) if hasattr(e, 'message') else str(e) # Если это список ошибок, берём первую if hasattr(e, 'messages'): error_message = e.messages[0] if e.messages else str(e) messages.error(request, error_message) except ValueError as e: # Ошибка в сигналах (например, не удалось создать Sale) # Транзакция откатилась, статус НЕ изменился messages.error(request, f'Ошибка при сохранении заказа: {e}') 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}") print("=== КОНЕЦ ОШИБОК ===\n") messages.error(request, 'Пожалуйста, исправьте ошибки в форме.') else: form = OrderForm(instance=order) formset = OrderItemFormSet(instance=order) context = { 'form': form, 'formset': formset, 'order': order, 'title': f'Редактирование заказа #{order.order_number}', 'button_text': 'Сохранить изменения', } 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) # === УПРАВЛЕНИЕ ТРАНЗАКЦИЯМИ === @login_required @require_http_methods(["POST"]) def transaction_add_payment(request, order_number): """ Добавление нового платежа к заказу. """ from orders.services.transaction_service import TransactionService order = get_object_or_404(Order, order_number=order_number) form = TransactionForm(request.POST) if form.is_valid(): try: # Создаём транзакцию платежа transaction = TransactionService.create_payment( order=order, amount=form.cleaned_data['amount'], payment_method=form.cleaned_data['payment_method'], user=request.user, notes=form.cleaned_data.get('notes') ) messages.success( request, f'Платёж на сумму {transaction.amount} руб. ' f'({transaction.payment_method.name}) успешно добавлен.' ) except (ValidationError, ValueError) as e: messages.error(request, f'Ошибка при добавлении платежа: {e}') else: # Показываем ошибки валидации for field, errors in form.errors.items(): for error in errors: messages.error(request, f'{field}: {error}') # Возвращаемся на страницу редактирования return redirect('orders:order-update', order_number=order.order_number) @login_required @require_http_methods(["POST"]) def transaction_add_refund(request, order_number): """ Добавление возврата по заказу. """ from orders.services.transaction_service import TransactionService order = get_object_or_404(Order, order_number=order_number) amount = request.POST.get('refund_amount') payment_method_id = request.POST.get('refund_payment_method') reason = request.POST.get('refund_reason') notes = request.POST.get('refund_notes') try: amount = Decimal(amount) payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) # Создаём транзакцию возврата transaction = TransactionService.create_refund( order=order, amount=amount, payment_method=payment_method, user=request.user, reason=reason, notes=notes ) messages.success( request, f'Возврат на сумму {transaction.amount} руб. ' f'({transaction.payment_method.name}) успешно создан.' ) except (ValidationError, ValueError) as e: messages.error(request, f'Ошибка при создании возврата: {e}') return redirect('orders:order-update', order_number=order.order_number) @login_required @require_http_methods(["POST"]) def transaction_delete(request, order_number, transaction_id): """ Удаление транзакции (не рекомендуется, лучше использовать refund). Оставлено для совместимости. """ order = get_object_or_404(Order, order_number=order_number) transaction_obj = get_object_or_404(Transaction, pk=transaction_id, order=order) # Сохраняем данные для сообщения transaction_info = f'{transaction_obj.get_transaction_type_display()} {transaction_obj.payment_method.name} на сумму {transaction_obj.amount} руб.' # Предупреждение: удаление транзакций нарушает историю transaction_obj.delete() # Пересчитываем баланс order.recalculate_amount_paid() messages.warning( request, f'Транзакция {transaction_info} удалена. ' f'Рекомендуем использовать "Возврат" вместо удаления.' ) return redirect('orders:order-update', order_number=order.order_number) # === AJAX ENDPOINTS === @require_http_methods(["POST"]) @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', order_number=order.order_number) # Вызываем сервис для оплаты из кошелька 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. Использует транзакцию, чтобы при ошибке в сигналах (например, при создании Sale) статус откатился вместе со всеми связанными изменениями. """ 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 == '': with transaction.atomic(): 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) # Оборачиваем в транзакцию, чтобы при ошибке в сигналах статус откатился with transaction.atomic(): 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 ValidationError as e: # Ошибка валидации (например, запрет смены статуса для возвращённого заказа) # Транзакция откатилась, статус НЕ изменился # Извлекаем чистое сообщение без служебных элементов error_message = str(e.message) if hasattr(e, 'message') else str(e) if hasattr(e, 'messages'): error_message = e.messages[0] if e.messages else str(e) return JsonResponse({'success': False, 'error': error_message}, status=400) except ValueError as e: # Ошибка в сигналах (например, не удалось создать Sale) # Транзакция откатилась, статус НЕ изменился return JsonResponse({'success': False, 'error': str(e)}, status=400) except Exception as e: return JsonResponse({'success': False, 'error': f'Server error: {str(e)}'}, status=500)