Исправить: показывать существующие платежи информационно при редактировании заказа

ПРОБЛЕМА:
При редактировании заказа с уже существующими платежами из кошелька,
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'
 Правильный подсчет итоговой суммы (существующие + новые)
This commit is contained in:
2025-11-29 02:14:54 +03:00
parent 65ab153f9e
commit f9e086fd89
2 changed files with 67 additions and 47 deletions

View File

@@ -608,55 +608,54 @@
<!-- Скрытые поля для formset management -->
{{ payment_formset.management_form }}
<!-- Контейнер для платежей -->
<div id="payments-container">
{% for payment_form in payment_formset %}
<div class="payment-form border rounded p-3 mb-3" data-form-index="{{ forloop.counter0 }}">
{{ payment_form.id }}
{{ payment_form.DELETE }}
<div class="row align-items-end">
<!-- Уже сохраненные платежи (информационно) -->
{% if order.pk and order.payments.exists %}
<div class="mb-4">
<h6 class="text-muted mb-3"><i class="bi bi-check-circle"></i> Проведенные платежи</h6>
{% for payment in order.payments.all %}
<div class="border rounded p-3 mb-2 bg-light">
<div class="row align-items-center">
<div class="col-md-4">
<div class="mb-2">
<label class="form-label">Способ оплаты</label>
{{ payment_form.payment_method }}
</div>
<small class="text-muted d-block">Способ оплаты</small>
<strong>{{ payment.payment_method.name }}</strong>
</div>
<div class="col-md-3">
<div class="mb-2">
<label class="form-label">Сумма</label>
{{ payment_form.amount }}
</div>
<small class="text-muted d-block">Сумма</small>
<strong class="text-success">{{ payment.amount|floatformat:2 }}&nbsp;руб.</strong>
</div>
<div class="col-md-4">
<div class="mb-2">
<label class="form-label">Примечания</label>
{{ payment_form.notes }}
<small class="text-muted d-block">Примечания</small>
<span class="text-muted">{{ payment.notes|default:"—" }}</span>
</div>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm w-100 remove-payment-btn">
<div class="col-md-1 text-end">
<form method="post" style="display: inline;" onsubmit="return confirm('Удалить этот платеж?');">
{% csrf_token %}
<input type="hidden" name="delete_payment_id" value="{{ payment.id }}">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Удалить платеж">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
{% if payment_form.errors %}
<div class="alert alert-danger mt-2">{{ payment_form.errors }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Контейнер для НОВЫХ платежей -->
<div id="payments-container">
<!-- Здесь будут добавляться новые платежи -->
</div>
<!-- Итоговая сумма платежей -->
<div id="payments-total-section" class="border-top pt-3 mt-3 mb-3">
<div class="row align-items-center">
<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 class="col-auto">
<h5 class="mb-0 text-success">
<span id="payments-total-value">0.00</span> руб.
<span id="payments-total-value">{{ order.amount_paid|default:"0.00"|floatformat:2 }}</span>&nbsp;руб.
</h5>
</div>
</div>
@@ -1631,20 +1630,29 @@ document.addEventListener('DOMContentLoaded', function() {
// Функция для расчета итоговой суммы платежей
function calculatePaymentsTotal() {
// 1. Считаем уже сохраненные платежи (информационные блоки)
let existingTotal = 0;
const existingPayments = document.querySelectorAll('.bg-light .text-success');
existingPayments.forEach((el) => {
const text = el.textContent.replace(/[^0-9.,]/g, '').replace(',', '.');
const amount = parseFloat(text) || 0;
existingTotal += amount;
});
// 2. Считаем новые платежи в formset
const visiblePaymentForms = Array.from(document.querySelectorAll('.payment-form'))
.filter(form => !form.classList.contains('deleted'));
let total = 0;
let newTotal = 0;
visiblePaymentForms.forEach((form) => {
const amountField = form.querySelector('[name$="-amount"]');
if (amountField) {
const amount = parseFloat(amountField.value.replace(',', '.')) || 0;
total += amount;
newTotal += amount;
}
});
return total;
return existingTotal + newTotal;
}
function updatePaymentsTotal() {
@@ -1735,19 +1743,6 @@ document.addEventListener('DOMContentLoaded', function() {
addPaymentBtn.addEventListener('click', addNewPayment);
}
// Добавляем обработчики удаления для существующих платежей
paymentsContainer.querySelectorAll('.remove-payment-btn').forEach(btn => {
btn.addEventListener('click', function() {
const form = this.closest('.payment-form');
removePayment(form);
});
});
// Добавляем обработчики для автоматического пересчета для существующих форм
paymentsContainer.querySelectorAll('[name$="-amount"]').forEach(field => {
field.addEventListener('input', updatePaymentsTotal);
});
// Инициализируем итоговую сумму
updatePaymentsTotal();

View File

@@ -157,6 +157,31 @@ 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')