Files
octopus/myproject/orders/views.py
Andrey Smakotin a97fc39a2c Рефакторинг: убрана финализация черновиков и улучшены шаблоны заказов
- Убран черновик как отдельная сущность с процессом финализации
- Черновик теперь просто обычный OrderStatus
- Удалены кнопки 'Сохранить как черновик' и 'Финализировать черновик'
- Унифицирована логика сохранения/обновления заказов для всех статусов

Улучшения шаблонов:
- Стандартизировано форматирование валюты через floatformat:2
- Исправлено отображение статуса (используется OrderStatus.label и color)
- Исправлено отображение способа оплаты (корректное использование ForeignKey)
- Добавлены иконки к заголовкам секций для лучшего UX
- Удалены избыточные console.log (~160 строк)
- Очищены комментарии и улучшена читаемость кода
- Убрано использование переменной is_draft в контексте
- Добавлена визуальная согласованность между шаблонами заказов
2025-11-29 01:51:19 +03:00

544 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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.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, prefix='payments')
if form.is_valid() and formset.is_valid() and payment_formset.is_valid():
# Сохраняем форму БЕЗ 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()
# Сохраняем платежи (устанавливаем created_by)
payment_formset.instance = order
unsaved_payments = payment_formset.save(commit=False)
for p in unsaved_payments:
if p.created_by_id is None:
p.created_by = request.user
p.order = order
p.save()
# Обрабатываем удалённые платежи
from customers.services.wallet_service import WalletService
for obj in payment_formset.deleted_objects:
# Если удаляем платёж из кошелька - возвращаем сумму обратно
if hasattr(obj, 'payment_method') and obj.payment_method and getattr(obj.payment_method, 'code', '') == 'account_balance':
WalletService.refund_wallet_payment(order, obj.amount, request.user)
obj.delete()
# Пересчитываем стоимость доставки если она не установлена вручную
delivery_cost = form.cleaned_data.get('delivery_cost')
if not delivery_cost or delivery_cost <= 0:
order.reset_delivery_cost()
# Пересчитываем сумму оплачено и итоговую стоимость
order.amount_paid = sum(p.amount for p in order.payments.all())
order.calculate_total()
order.update_payment_status()
# Обрабатываем переплату (если amount_paid > total_amount)
WalletService.add_overpayment(order, request.user)
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(prefix='payments')
context = {
'form': form,
'formset': formset,
'payment_formset': payment_formset,
'preselected_customer': preselected_customer,
'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)
if request.method == 'POST':
form = OrderForm(request.POST, instance=order)
formset = OrderItemFormSet(request.POST, instance=order)
payment_formset = PaymentFormSet(request.POST, instance=order, prefix='payments')
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
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()
# Сохраняем платежи (устанавливаем created_by)
payment_formset.instance = order
unsaved_payments = payment_formset.save(commit=False)
for p in unsaved_payments:
if p.created_by_id is None:
p.created_by = request.user
p.order = order
p.save()
# Обрабатываем удалённые платежи
from customers.services.wallet_service import WalletService
for obj in payment_formset.deleted_objects:
# Если удаляем платёж из кошелька - возвращаем сумму обратно
if hasattr(obj, 'payment_method') and obj.payment_method and getattr(obj.payment_method, 'code', '') == 'account_balance':
WalletService.refund_wallet_payment(order, obj.amount, request.user)
obj.delete()
# Пересчитываем сумму оплачено и итоговую стоимость
order.amount_paid = sum(p.amount for p in order.payments.all())
order.calculate_total()
order.update_payment_status()
# Обрабатываем переплату (если amount_paid > total_amount)
WalletService.add_overpayment(order, request.user)
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, prefix='payments')
context = {
'form': form,
'formset': formset,
'payment_formset': payment_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)
# === 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.
"""
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)