From 84ed3a0c7ddce373c36adcf6e0420bb91756cf17 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 29 Nov 2025 02:27:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3:=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B5=20endpoints=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0=D1=82=D0=B5=D0=B6=D0=B0=D0=BC=D0=B8=20(Djang?= =?UTF-8?q?o=20best=20practices)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ПРОБЛЕМА: Использование 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//payments/add|/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: чище и понятнее --- .../orders/templates/orders/order_form.html | 328 ++++-------------- myproject/orders/templatetags/__init__.py | 2 +- myproject/orders/templatetags/orders_tags.py | 14 + myproject/orders/urls.py | 4 + myproject/orders/views.py | 154 ++++---- 5 files changed, 160 insertions(+), 342 deletions(-) create mode 100644 myproject/orders/templatetags/orders_tags.py diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index d12b763..53bf0d4 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -571,44 +571,32 @@
-
+
Оплата
-
{% if order.customer %} -
-
- Кошелёк клиента: - {% if order.customer.wallet_balance > 0 %} - {{ order.customer.wallet_balance|floatformat:2 }} руб. - {% else %} - 0.00 руб. - {% endif %} - Остаток к оплате: {{ order.amount_due|floatformat:2 }} руб. -
- {% if order.customer.wallet_balance > 0 and order.amount_due > 0 %} -
- -
- - +
+
+
+ Кошелёк клиента: + {% if order.customer.wallet_balance > 0 %} + {{ order.customer.wallet_balance|floatformat:2 }} руб. + {% else %} + 0.00 руб. + {% endif %} +
+
+ Остаток к оплате: + {{ order.amount_due|floatformat:2 }} руб.
- {% endif %}
{% endif %} - - {{ payment_formset.management_form }} - - + {% if order.pk and order.payments.exists %}
Проведенные платежи
@@ -628,13 +616,14 @@ {{ payment.notes|default:"—" }}
- +
+ {% csrf_token %} + +
@@ -642,251 +631,64 @@
{% endif %} - -
- + + {% if order.pk %} +
+
Добавить новый платеж
+
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ {% else %} +
+ Сначала создайте заказ, затем вы сможете добавлять платежи. +
+ {% endif %} -
+ {% if order.pk %} +

Всего внесено:

- {{ order.amount_paid|default:"0.00"|floatformat:2 }} руб. + {{ order.amount_paid|default:"0.00"|floatformat:2 }} руб.
- - - + {% endif %}
- - - -
Дополнительно
diff --git a/myproject/orders/templatetags/__init__.py b/myproject/orders/templatetags/__init__.py index 431ce7f..72ebbd4 100644 --- a/myproject/orders/templatetags/__init__.py +++ b/myproject/orders/templatetags/__init__.py @@ -1 +1 @@ -# Инициализация пакета templatetags +# Template tags package diff --git a/myproject/orders/templatetags/orders_tags.py b/myproject/orders/templatetags/orders_tags.py new file mode 100644 index 0000000..7d1476b --- /dev/null +++ b/myproject/orders/templatetags/orders_tags.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from django import template +from orders.models import PaymentMethod + +register = template.Library() + + +@register.simple_tag +def get_payment_methods(): + """ + Получить список активных способов оплаты. + Использование: {% get_payment_methods as payment_methods %} + """ + return PaymentMethod.objects.filter(is_active=True).order_by('order', 'name') diff --git a/myproject/orders/urls.py b/myproject/orders/urls.py index 01d566d..c597063 100644 --- a/myproject/orders/urls.py +++ b/myproject/orders/urls.py @@ -11,6 +11,10 @@ urlpatterns = [ path('/edit/', views.order_update, name='order-update'), path('/delete/', views.order_delete, name='order-delete'), + # Payment Management + path('/payments/add/', views.payment_add, name='payment-add'), + path('/payments//delete/', views.payment_delete, name='payment-delete'), + # AJAX endpoints path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'), diff --git a/myproject/orders/views.py b/myproject/orders/views.py index 472285b..65681c2 100644 --- a/myproject/orders/views.py +++ b/myproject/orders/views.py @@ -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"])