- Modified order_create view to read customer from GET parameter - Pass preselected_customer to template context - Template renders select with preselected option for Select2 - Fixed draft creation timing with callback after Select2 initialization - Auto-create draft when customer is preselected from URL - Graceful handling if customer not found or invalid ID
665 lines
24 KiB
Python
665 lines
24 KiB
Python
# -*- 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, pk):
|
||
"""Детальная информация о заказе"""
|
||
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'),
|
||
pk=pk
|
||
)
|
||
|
||
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', pk=order.pk)
|
||
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, pk):
|
||
"""Редактирование заказа"""
|
||
order = get_object_or_404(Order, pk=pk)
|
||
|
||
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', pk=order.pk)
|
||
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', pk=order.pk)
|
||
else:
|
||
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, pk):
|
||
"""Удаление заказа с подтверждением"""
|
||
order = get_object_or_404(Order, pk=pk)
|
||
|
||
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, pk):
|
||
"""
|
||
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(pk=pk)
|
||
except Order.DoesNotExist:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Заказ не найден'
|
||
}, status=404)
|
||
|
||
# Используем DraftOrderService для обновления
|
||
order = DraftOrderService.update_draft(
|
||
order_id=pk,
|
||
user=request.user,
|
||
data=data
|
||
)
|
||
|
||
# Обрабатываем позиции заказа, если они переданы
|
||
if 'items' in data:
|
||
# Удаляем существующие позиции
|
||
order.items.all().delete()
|
||
|
||
# Создаем новые позиции
|
||
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,
|
||
'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.pk}/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, pk):
|
||
"""
|
||
Применение оплаты из кошелька клиента к заказу.
|
||
Вызывается через POST-запрос с суммой для списания.
|
||
"""
|
||
if request.method != 'POST':
|
||
return redirect('orders:order-detail', pk=pk)
|
||
|
||
order = get_object_or_404(Order, pk=pk)
|
||
|
||
# Получаем запрашиваемую сумму из формы
|
||
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', pk=pk)
|