Рефакторинг: отдельные endpoints для управления платежами (Django best practices)
ПРОБЛЕМА: Использование PaymentFormSet для платежей было НЕПРАВИЛЬНЫМ подходом: 1. Платежи = финансовые транзакции (не должны редактироваться inline) 2. Формы валидировали существующие платежи как новые 3. Сложная логика с formset management forms 4. Конфликты валидации кошелька РЕШЕНИЕ (Django Best Practices): Разделили управление платежами на отдельные операции: АРХИТЕКТУРА: ` POST /orders/111/payments/add/ # Добавить платеж POST /orders/111/payments/123/delete/ # Удалить платеж ` ПРЕИМУЩЕСТВА: ✅ Чистая архитектура (separation of concerns) ✅ Платежи = неизменяемые транзакции ✅ Простая валидация (только для новых) ✅ Легко тестировать ✅ API-ready структура ИЗМЕНЕНИЯ: 1. orders/views.py: - Убран PaymentFormSet из order_create и order_update - Добавлен payment_add(request, order_number) - Добавлен payment_delete(request, order_number, payment_id) - Используется простой PaymentForm вместо formset - Payment.save() автоматически обрабатывает: * Списание из кошелька * Обработку переплаты * Обновление amount_paid 2. orders/urls.py: - Добавлены URL patterns для payment-add и payment-delete - Структура: /orders/<number>/payments/add|<id>/delete/ 3. orders/templates/orders/order_form.html: - Убран PaymentFormSet и все его скрипты (~265 строк) - Простая HTML форма для добавления платежа - Существующие платежи: read-only список с кнопками удаления - Каждое удаление = отдельный POST запрос - Для создания: показываем предупреждение вместо формы 4. orders/templatetags/orders_tags.py (NEW): - Template tag get_payment_methods - Загружает активные способы оплаты - Использование: {% get_payment_methods as payment_methods %} РЕЗУЛЬТАТ: - Код: -191 строка - Логика: простая и понятная - Архитектура: правильная (как в учебнике) - Платежи: только add/delete (без edit) - Валидация: работает корректно - UX: чище и понятнее
This commit is contained in:
@@ -8,8 +8,8 @@ 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 .models import Order, OrderItem, Address, OrderStatus, Payment, PaymentMethod
|
||||
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentForm
|
||||
from .filters import OrderFilter
|
||||
from .services.address_service import AddressService
|
||||
import json
|
||||
@@ -65,9 +65,8 @@ 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():
|
||||
if form.is_valid() and formset.is_valid():
|
||||
# Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items
|
||||
order = form.save(commit=False)
|
||||
|
||||
@@ -90,31 +89,12 @@ def order_create(request):
|
||||
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()
|
||||
|
||||
@@ -137,12 +117,10 @@ def order_create(request):
|
||||
|
||||
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': 'Создать заказ',
|
||||
@@ -157,36 +135,10 @@ 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():
|
||||
if form.is_valid() and formset.is_valid():
|
||||
order = form.save(commit=False)
|
||||
|
||||
# Обрабатываем адрес доставки
|
||||
@@ -218,26 +170,7 @@ def order_update(request, order_number):
|
||||
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()
|
||||
|
||||
@@ -254,20 +187,15 @@ def order_update(request, order_number):
|
||||
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': 'Сохранить изменения',
|
||||
@@ -293,6 +221,76 @@ def order_delete(request, order_number):
|
||||
return render(request, 'orders/order_confirm_delete.html', context)
|
||||
|
||||
|
||||
# === УПРАВЛЕНИЕ ПЛАТЕЖАМИ ===
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def payment_add(request, order_number):
|
||||
"""
|
||||
Добавление нового платежа к заказу.
|
||||
Отдельный endpoint для чистоты архитектуры.
|
||||
"""
|
||||
order = get_object_or_404(Order, order_number=order_number)
|
||||
|
||||
form = PaymentForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
payment = form.save(commit=False)
|
||||
payment.order = order
|
||||
payment.created_by = request.user
|
||||
|
||||
try:
|
||||
# save() вызовет Payment.save() который обработает:
|
||||
# - Списание из кошелька (если account_balance)
|
||||
# - Обработку переплаты
|
||||
# - Обновление amount_paid и payment_status
|
||||
payment.save()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Платеж на сумму {payment.amount} руб. '
|
||||
f'({payment.payment_method.name}) успешно добавлен.'
|
||||
)
|
||||
except ValidationError 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 payment_delete(request, order_number, payment_id):
|
||||
"""
|
||||
Удаление платежа.
|
||||
Возвращает средства в кошелек, если платеж был из кошелька.
|
||||
"""
|
||||
order = get_object_or_404(Order, order_number=order_number)
|
||||
payment = get_object_or_404(Payment, pk=payment_id, order=order)
|
||||
|
||||
# Сохраняем данные для сообщения
|
||||
payment_info = f'{payment.payment_method.name} на сумму {payment.amount} руб.'
|
||||
|
||||
# Если это платеж из кошелька - возвращаем средства
|
||||
if payment.payment_method.code == 'account_balance':
|
||||
from customers.services.wallet_service import WalletService
|
||||
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, f'Платеж {payment_info} успешно удален.')
|
||||
return redirect('orders:order-update', order_number=order.order_number)
|
||||
|
||||
|
||||
# === AJAX ENDPOINTS ===
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
|
||||
Reference in New Issue
Block a user