Рефакторинг: отдельные 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:
2025-11-29 02:27:50 +03:00
parent ee002d5fed
commit 84ed3a0c7d
5 changed files with 160 additions and 342 deletions

View File

@@ -571,44 +571,32 @@
<!-- Оплата (смешанная оплата) --> <!-- Оплата (смешанная оплата) -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header">
<h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5> <h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5>
<button type="button" class="btn btn-sm btn-success" id="add-payment-btn">
<i class="bi bi-plus-circle"></i> Добавить платеж
</button>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Блок кошелька клиента --> <!-- Блок кошелька клиента -->
{% if order.customer %} {% if order.customer %}
<div class="alert alert-info d-flex justify-content-between align-items-center"> <div class="alert alert-info mb-3">
<div class="d-flex justify-content-between align-items-center">
<div> <div>
<strong><i class="bi bi-wallet2"></i> Кошелёк клиента:</strong> <strong><i class="bi bi-wallet2"></i> Кошелёк клиента:</strong>
{% if order.customer.wallet_balance > 0 %} {% if order.customer.wallet_balance > 0 %}
<span class="text-success fw-bold">{{ order.customer.wallet_balance|floatformat:2 }} руб.</span> <span class="text-success fw-bold">{{ order.customer.wallet_balance|floatformat:2 }}&nbsp;руб.</span>
{% else %} {% else %}
<span class="text-muted">0.00 руб.</span> <span class="text-muted">0.00&nbsp;руб.</span>
{% endif %} {% endif %}
<span class="ms-3">Остаток к оплате: <strong>{{ order.amount_due|floatformat:2 }} руб.</strong></span>
</div> </div>
{% if order.customer.wallet_balance > 0 and order.amount_due > 0 %} <div>
<div class="d-flex gap-2"> <strong>Остаток к оплате:</strong>
<button type="button" class="btn btn-primary btn-sm" id="apply-wallet-max-btn"> <span class="text-primary fw-bold">{{ order.amount_due|floatformat:2 }}&nbsp;руб.</span>
Учесть максимум
</button>
<div class="input-group" style="max-width: 280px;">
<input type="number" step="0.01" min="0" class="form-control form-control-sm" id="apply-wallet-amount-input" placeholder="Сумма из кошелька">
<button type="button" class="btn btn-outline-primary btn-sm" id="apply-wallet-amount-btn">Учесть сумму</button>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
{% endif %} {% endif %}
<!-- Скрытые поля для formset management --> <!-- Уже сохраненные платежи -->
{{ payment_formset.management_form }}
<!-- Уже сохраненные платежи (информационно) -->
{% if order.pk and order.payments.exists %} {% if order.pk and order.payments.exists %}
<div class="mb-4"> <div class="mb-4">
<h6 class="text-muted mb-3"><i class="bi bi-check-circle"></i> Проведенные платежи</h6> <h6 class="text-muted mb-3"><i class="bi bi-check-circle"></i> Проведенные платежи</h6>
@@ -628,13 +616,14 @@
<span class="text-muted">{{ payment.notes|default:"—" }}</span> <span class="text-muted">{{ payment.notes|default:"—" }}</span>
</div> </div>
<div class="col-md-1 text-end"> <div class="col-md-1 text-end">
<button type="button" class="btn btn-outline-danger btn-sm delete-existing-payment-btn" <form method="post" action="{% url 'orders:payment-delete' order.order_number payment.id %}" style="display: inline;">
data-payment-id="{{ payment.id }}" {% csrf_token %}
data-payment-name="{{ payment.payment_method.name }}" <button type="submit" class="btn btn-outline-danger btn-sm"
data-payment-amount="{{ payment.amount|floatformat:2 }}" onclick="return confirm('Удалить платеж {{ payment.payment_method.name }} на сумму {{ payment.amount|floatformat:2 }} руб.?');"
title="Удалить платеж"> title="Удалить платеж">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</form>
</div> </div>
</div> </div>
</div> </div>
@@ -642,250 +631,63 @@
</div> </div>
{% endif %} {% endif %}
<!-- Контейнер для НОВЫХ платежей --> <!-- Форма добавления нового платежа -->
<div id="payments-container"> {% if order.pk %}
<!-- Здесь будут добавляться новые платежи --> <div class="border rounded p-3 bg-white">
<h6 class="mb-3"><i class="bi bi-plus-circle"></i> Добавить новый платеж</h6>
<form method="post" action="{% url 'orders:payment-add' order.order_number %}" id="payment-add-form">
{% csrf_token %}
<div class="row align-items-end">
<div class="col-md-4">
<label class="form-label">Способ оплаты</label>
<select name="payment_method" class="form-select" required>
<option value="">---------</option>
{% load orders_tags %}
{% get_payment_methods as payment_methods %}
{% for pm in payment_methods %}
<option value="{{ pm.id }}">{{ pm.name }}</option>
{% endfor %}
</select>
</div> </div>
<div class="col-md-3">
<label class="form-label">Сумма</label>
<input type="number" name="amount" step="0.01" min="0.01" class="form-control" placeholder="0.00" required>
</div>
<div class="col-md-3">
<label class="form-label">Примечания</label>
<input type="text" name="notes" class="form-control" placeholder="Опционально">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-success w-100">
<i class="bi bi-plus-lg"></i> Добавить
</button>
</div>
</div>
</form>
</div>
{% else %}
<div class="alert alert-warning mb-0">
<i class="bi bi-info-circle"></i> Сначала создайте заказ, затем вы сможете добавлять платежи.
</div>
{% endif %}
<!-- Итоговая сумма платежей --> <!-- Итоговая сумма платежей -->
<div id="payments-total-section" class="border-top pt-3 mt-3 mb-3"> {% if order.pk %}
<div class="border-top pt-3 mt-3">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<p class="mb-0 text-muted"><i class="bi bi-cash-stack"></i> Всего внесено:</p> <p class="mb-0 text-muted"><i class="bi bi-cash-stack"></i> Всего внесено:</p>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<h5 class="mb-0 text-success"> <h5 class="mb-0 text-success">
<span id="payments-total-value">{{ order.amount_paid|default:"0.00"|floatformat:2 }}</span>&nbsp;руб. {{ order.amount_paid|default:"0.00"|floatformat:2 }}&nbsp;руб.
</h5> </h5>
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<!-- Скрытый шаблон для новых платежей -->
<template id="empty-payment-form-template">
<div class="payment-form border rounded p-3 mb-3" data-form-index="__prefix__">
<input type="hidden" name="payments-__prefix__-id" id="id_payments-__prefix__-id" form="order-form">
<input type="checkbox" name="payments-__prefix__-DELETE" id="id_payments-__prefix__-DELETE" form="order-form" style="display: none;">
<div class="row align-items-end">
<div class="col-md-4">
<div class="mb-2">
<label class="form-label">Способ оплаты</label>
<select name="payments-__prefix__-payment_method" class="form-select" id="id_payments-__prefix__-payment_method" form="order-form">
<option value="">---------</option>
{% for pm in payment_formset.forms.0.fields.payment_method.queryset %}
<option value="{{ pm.id }}">{{ pm.name }}</option>
{% endfor %}
</select>
</div> </div>
</div> </div>
<div class="col-md-3">
<div class="mb-2">
<label class="form-label">Сумма</label>
<input type="number" name="payments-__prefix__-amount" step="0.01" min="0" class="form-control" placeholder="Сумма платежа" id="id_payments-__prefix__-amount" form="order-form">
</div>
</div>
<div class="col-md-4">
<div class="mb-2">
<label class="form-label">Примечания</label>
<textarea name="payments-__prefix__-notes" class="form-control" rows="1" placeholder="Примечания к платежу (опционально)" id="id_payments-__prefix__-notes" form="order-form"></textarea>
</div>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm w-100 remove-payment-btn">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</template>
</div>
</div>
<script>
(function() {
const walletBalance = parseFloat("{{ order.customer.wallet_balance|default:'0' }}".replace(',', '.')) || 0;
const amountDue = parseFloat("{{ order.amount_due|default:'0' }}".replace(',', '.')) || 0;
function addPaymentRow() {
const totalFormsInput = document.querySelector('input[name="payments-TOTAL_FORMS"]');
const idx = parseInt(totalFormsInput.value, 10);
const tpl = document.getElementById('empty-payment-form-template').innerHTML.replace(/__prefix__/g, idx);
const container = document.getElementById('payments-container');
const wrapper = document.createElement('div');
wrapper.innerHTML = tpl.trim();
container.appendChild(wrapper.firstElementChild);
totalFormsInput.value = String(idx + 1);
return container.querySelector(`.payment-form[data-form-index="${idx}"]`);
}
function selectAccountBalance(selectEl) {
if (!selectEl) return;
const options = Array.from(selectEl.options);
const target = options.find(o => o.textContent.trim() === 'С баланса счёта');
if (target) {
selectEl.value = target.value;
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
}
}
function applyWallet(amount) {
if (!amount || amount <= 0) {
alert('Введите сумму больше 0.');
return;
}
if (amount > walletBalance) {
alert(`Недостаточно средств в кошельке. Доступно ${walletBalance.toFixed(2)} руб.`);
return;
}
if (amount > amountDue) {
alert(`Сумма превышает остаток к оплате (${amountDue.toFixed(2)} руб.).`);
return;
}
// Всегда добавляем новую строку платежа
const formEl = addPaymentRow();
const sel = formEl.querySelector('select[id^="id_payments-"][id$="-payment_method"]');
const amt = formEl.querySelector('input[id^="id_payments-"][id$="-amount"]');
const notes = formEl.querySelector('textarea[id^="id_payments-"][id$="-notes"]');
// Загружаем список способов оплаты
if (sel) {
fetch('/products/api/payment-methods/')
.then(response => response.json())
.then(data => {
sel.innerHTML = '<option value="">---------</option>';
data.forEach(method => {
const option = document.createElement('option');
option.value = method.id;
option.textContent = method.name;
sel.appendChild(option);
});
// После загрузки устанавливаем "С баланса счёта"
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].textContent.trim() === 'С баланса счёта') {
sel.value = sel.options[i].value;
break;
}
}
})
.catch(error => {
console.error('Error loading payment methods:', error);
});
}
// Проставляем сумму
if (amt) {
const maxUsable = Math.min(walletBalance, amountDue);
const finalAmount = Math.min(amount, maxUsable);
amt.value = finalAmount.toFixed(2);
amt.setAttribute('max', maxUsable.toFixed(2));
}
// Небольшая подсказка в примечания
if (notes && !notes.value) {
notes.value = 'Оплата из кошелька';
}
// Добавляем обработчик удаления
const removeBtn = formEl.querySelector('.remove-payment-btn');
if (removeBtn) {
removeBtn.addEventListener('click', function() {
if (!confirm('Вы действительно хотите удалить этот платеж?')) {
return;
}
const deleteCheckbox = formEl.querySelector('input[name$="-DELETE"]');
const idField = formEl.querySelector('input[name$="-id"]');
if (idField && idField.value) {
deleteCheckbox.checked = true;
formEl.classList.add('deleted');
formEl.style.display = 'none';
} else {
formEl.remove();
}
});
}
}
// Обработчики кнопок применения кошелька
document.addEventListener('DOMContentLoaded', function() {
const applyMaxBtn = document.getElementById('apply-wallet-max-btn');
const applyAmountBtn = document.getElementById('apply-wallet-amount-btn');
const amountInput = document.getElementById('apply-wallet-amount-input');
if (applyMaxBtn) {
applyMaxBtn.addEventListener('click', function() {
const maxUsable = Math.min(walletBalance, amountDue);
applyWallet(maxUsable);
});
}
if (applyAmountBtn && amountInput) {
applyAmountBtn.addEventListener('click', function() {
const val = parseFloat((amountInput.value || '0').replace(',', '.')) || 0;
applyWallet(val);
});
}
});
// Автозаполнение при выборе "С баланса счёта"
document.getElementById('payments-container').addEventListener('change', function(e) {
const sel = e.target;
if (sel.tagName === 'SELECT') {
const label = sel.options[sel.selectedIndex]?.textContent?.trim() || '';
if (label === 'С баланса счёта') {
const wrap = sel.closest('.payment-form');
const amt = wrap.querySelector('input[id^="id_payments-"][id$="-amount"]');
if (amt) {
const maxUsable = Math.min(walletBalance, amountDue);
amt.value = maxUsable.toFixed(2);
amt.setAttribute('max', maxUsable.toFixed(2));
}
}
}
});
})();
</script>
<script>
// Обработчик удаления существующих платежей
document.addEventListener('DOMContentLoaded', function() {
const deleteButtons = document.querySelectorAll('.delete-existing-payment-btn');
deleteButtons.forEach(button => {
button.addEventListener('click', function() {
const paymentId = this.dataset.paymentId;
const paymentName = this.dataset.paymentName;
const paymentAmount = this.dataset.paymentAmount;
if (!confirm(`Удалить платеж "${paymentName}" на сумму ${paymentAmount} руб.?`)) {
return;
}
// Создаем скрытую форму для отправки
const form = document.createElement('form');
form.method = 'POST';
form.action = window.location.href;
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
form.appendChild(csrfInput);
const paymentIdInput = document.createElement('input');
paymentIdInput.type = 'hidden';
paymentIdInput.name = 'delete_payment_id';
paymentIdInput.value = paymentId;
form.appendChild(paymentIdInput);
document.body.appendChild(form);
form.submit();
});
});
});
</script>
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">

View File

@@ -1 +1 @@
# Инициализация пакета templatetags # Template tags package

View File

@@ -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')

View File

@@ -11,6 +11,10 @@ urlpatterns = [
path('<int:order_number>/edit/', views.order_update, name='order-update'), path('<int:order_number>/edit/', views.order_update, name='order-update'),
path('<int:order_number>/delete/', views.order_delete, name='order-delete'), path('<int:order_number>/delete/', views.order_delete, name='order-delete'),
# Payment Management
path('<int:order_number>/payments/add/', views.payment_add, name='payment-add'),
path('<int:order_number>/payments/<int:payment_id>/delete/', views.payment_delete, name='payment-delete'),
# AJAX endpoints # AJAX endpoints
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'), path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),

View File

@@ -8,8 +8,8 @@ from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from decimal import Decimal from decimal import Decimal
from .models import Order, OrderItem, Address, OrderStatus from .models import Order, OrderItem, Address, OrderStatus, Payment, PaymentMethod
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentFormSet from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentForm
from .filters import OrderFilter from .filters import OrderFilter
from .services.address_service import AddressService from .services.address_service import AddressService
import json import json
@@ -65,9 +65,8 @@ def order_create(request):
if request.method == 'POST': if request.method == 'POST':
form = OrderForm(request.POST) form = OrderForm(request.POST)
formset = OrderItemFormSet(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 # Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items
order = form.save(commit=False) order = form.save(commit=False)
@@ -90,31 +89,12 @@ def order_create(request):
formset.instance = order formset.instance = order
formset.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()
# Пересчитываем стоимость доставки если она не установлена вручную # Пересчитываем стоимость доставки если она не установлена вручную
delivery_cost = form.cleaned_data.get('delivery_cost') delivery_cost = form.cleaned_data.get('delivery_cost')
if not delivery_cost or delivery_cost <= 0: if not delivery_cost or delivery_cost <= 0:
order.reset_delivery_cost() order.reset_delivery_cost()
# Пересчитываем сумму оплачено и итоговую стоимость # Пересчитываем итоговую стоимость
order.amount_paid = sum(p.amount for p in order.payments.all())
order.calculate_total() order.calculate_total()
order.update_payment_status() order.update_payment_status()
@@ -137,12 +117,10 @@ def order_create(request):
form = OrderForm(initial=initial_data) form = OrderForm(initial=initial_data)
formset = OrderItemFormSet() formset = OrderItemFormSet()
payment_formset = PaymentFormSet(prefix='payments')
context = { context = {
'form': form, 'form': form,
'formset': formset, 'formset': formset,
'payment_formset': payment_formset,
'preselected_customer': preselected_customer, 'preselected_customer': preselected_customer,
'title': 'Создание заказа', 'title': 'Создание заказа',
'button_text': 'Создать заказ', 'button_text': 'Создать заказ',
@@ -157,36 +135,10 @@ def order_update(request, order_number):
order = get_object_or_404(Order, order_number=order_number) order = get_object_or_404(Order, order_number=order_number)
if request.method == 'POST': 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) form = OrderForm(request.POST, instance=order)
formset = OrderItemFormSet(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) order = form.save(commit=False)
# Обрабатываем адрес доставки # Обрабатываем адрес доставки
@@ -218,26 +170,7 @@ def order_update(request, order_number):
order.save() order.save()
formset.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.calculate_total()
order.update_payment_status() order.update_payment_status()
@@ -254,20 +187,15 @@ def order_update(request, order_number):
for i, item_form in enumerate(formset): for i, item_form in enumerate(formset):
if item_form.errors: if item_form.errors:
print(f" Item form {i} errors: {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") print("=== КОНЕЦ ОШИБОК ===\n")
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.') messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
else: else:
form = OrderForm(instance=order) form = OrderForm(instance=order)
formset = OrderItemFormSet(instance=order) formset = OrderItemFormSet(instance=order)
payment_formset = PaymentFormSet(instance=order, prefix='payments')
context = { context = {
'form': form, 'form': form,
'formset': formset, 'formset': formset,
'payment_formset': payment_formset,
'order': order, 'order': order,
'title': f'Редактирование заказа #{order.order_number}', 'title': f'Редактирование заказа #{order.order_number}',
'button_text': 'Сохранить изменения', 'button_text': 'Сохранить изменения',
@@ -293,6 +221,76 @@ def order_delete(request, order_number):
return render(request, 'orders/order_confirm_delete.html', context) 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 === # === AJAX ENDPOINTS ===
@require_http_methods(["POST"]) @require_http_methods(["POST"])