ПРОБЛЕМА: При редактировании заказа с уже существующими платежами из кошелька, formset пытался валидировать ВСЕ платежи как новые, включая уже проведенные. Это вызывало ошибки валидации кошелька, даже когда пользователь просто хотел добавить новый платеж другим методом. РЕШЕНИЕ: Разделили отображение платежей на две части: 1. УЖЕ ПРОВЕДЕННЫЕ ПЛАТЕЖИ (информационный блок): - Показываются в виде read-only карточек (bg-light) - Не проходят через formset валидацию - Можно удалить через отдельную форму с POST-запросом - Содержат: способ оплаты, сумму, примечания, кнопку удаления 2. НОВЫЕ ПЛАТЕЖИ (formset): - Добавляются через кнопку 'Добавить платеж' - Проходят валидацию только для новых записей - Контейнер изначально пустой (#payments-container) ИЗМЕНЕНИЯ: orders/templates/orders/order_form.html: - Добавлен блок 'Проведенные платежи' с информационным отображением - Каждый существующий платеж с формой удаления (delete_payment_id) - Контейнер для новых платежей теперь пустой при загрузке - Обновлен calculatePaymentsTotal(): считает существующие + новые - Убраны обработчики для несуществующих элементов formset - Итоговая сумма инициализируется из order.amount_paid orders/views.py (order_update): - Добавлена обработка delete_payment_id из POST - При удалении платежа из кошелька - возврат средств через WalletService - Пересчет amount_paid после удаления - Редирект обратно в форму после удаления РЕЗУЛЬТАТ: ✅ Существующие платежи не валидируются повторно ✅ Можно свободно добавлять новые платежи любым методом ✅ Удаление существующих платежей работает корректно ✅ Возврат в кошелек при удалении платежа 'account_balance' ✅ Правильный подсчет итоговой суммы (существующие + новые)
563 lines
23 KiB
Python
563 lines
23 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.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()
|
||
|
||
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':
|
||
# Обработка удаления существующего платежа
|
||
delete_payment_id = request.POST.get('delete_payment_id')
|
||
if delete_payment_id:
|
||
try:
|
||
from orders.models import Payment
|
||
from customers.services.wallet_service import WalletService
|
||
|
||
payment = Payment.objects.get(pk=delete_payment_id, order=order)
|
||
|
||
# Если это платеж из кошелька - возвращаем средства
|
||
if payment.payment_method and payment.payment_method.code == 'account_balance':
|
||
WalletService.refund_wallet_payment(order, payment.amount, request.user)
|
||
|
||
payment.delete()
|
||
|
||
# Пересчитываем сумму оплаты
|
||
order.amount_paid = sum(p.amount for p in order.payments.all())
|
||
order.update_payment_status()
|
||
|
||
messages.success(request, 'Платеж успешно удален.')
|
||
return redirect('orders:order-update', order_number=order.order_number)
|
||
except Payment.DoesNotExist:
|
||
messages.error(request, 'Платеж не найден.')
|
||
return redirect('orders:order-update', order_number=order.order_number)
|
||
|
||
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()
|
||
|
||
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)
|