Упрощена форма оплаты заказа: единая форма платежа/возврата с переключателем режимов

This commit is contained in:
2025-11-29 22:23:42 +03:00
parent e10faf697f
commit 4d197790fc

View File

@@ -595,21 +595,24 @@
<!-- Правая колонка: Оплата --> <!-- Правая колонка: Оплата -->
<div class="col-lg-5"> <div class="col-lg-5">
<!-- Оплата и возвраты -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5> <h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5>
{% if order.pk and order.amount_paid > 0 %} {% if order.pk %}
<span class="badge bg-secondary">Доступно для возврата: {{ order.amount_paid|floatformat:2 }} руб.</span> {% if order.amount_due <= 0 %}
<span class="badge bg-success fs-5">Оплачено</span>
{% elif order.amount_paid > 0 %}
<span class="badge bg-warning text-dark fs-5">Частично оплачено</span>
{% else %}
<span class="badge bg-secondary fs-5">Не оплачено</span>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="card-body">
<!-- Блок статистики --> <div class="card-body">
{% if order.pk %} {% if order.pk %}
<!-- Предупреждение о переплате --> <!-- Предупреждение о переплате -->
{% if order.amount_paid > order.total_amount %} {% if order.amount_paid > order.total_amount %}
<div class="alert alert-warning mb-3"> <div class="alert alert-warning mb-3">
@@ -618,61 +621,127 @@
Оплачено <strong>{{ order.amount_paid|floatformat:2 }} руб.</strong>, Оплачено <strong>{{ order.amount_paid|floatformat:2 }} руб.</strong>,
сумма заказа <strong>{{ order.total_amount|floatformat:2 }} руб.</strong><br> сумма заказа <strong>{{ order.total_amount|floatformat:2 }} руб.</strong><br>
Переплата: <strong class="text-danger">{{ order.overpayment|floatformat:2 }} руб.</strong><br> Переплата: <strong class="text-danger">{{ order.overpayment|floatformat:2 }} руб.</strong><br>
Создайте возврат в кошелёк клиента во вкладке «Создать возврат» ниже. Создайте возврат средств ниже.
</small> </small>
</div> </div>
{% endif %} {% endif %}
<div class="row mb-3"> <!-- Сумма заказа и остаток -->
<div class="col-md-6"> <div class="row g-2 mb-3">
<div class="card border-primary"> <div class="col-4">
<div class="card h-100">
<div class="card-body text-center p-2"> <div class="card-body text-center p-2">
<small class="text-muted d-block">Сумма заказа</small> <small class="text-muted d-block mb-1">Сумма заказа</small>
<h6 class="mb-0 text-primary">{{ order.total_amount|floatformat:2 }} руб.</h6> <h4 class="text-primary mb-0">{{ order.total_amount|floatformat:2 }} руб.</h4>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-4">
<div class="card border-success"> <div class="card h-100 border-{% if order.amount_due > 0 %}warning{% else %}success{% endif %} border-3 shadow-sm">
<div class="card-body text-center p-2"> <div class="card-body text-center p-2">
<small class="text-muted d-block">Оплачено</small> <small class="text-muted d-block mb-1">Остаток к оплате</small>
<h6 class="mb-0 text-success">{{ order.amount_paid|floatformat:2 }} руб.</h6> <h4 class="text-{% if order.amount_due > 0 %}warning{% else %}success{% endif %} mb-0 fw-bold">{{ order.amount_due|floatformat:2 }} руб.</h4>
</div>
</div>
</div>
<div class="col-4">
<div class="card h-100">
<div class="card-body text-center p-2">
<small class="text-muted d-block mb-1">Уже оплачено</small>
<h4 class="text-success mb-0">{{ order.amount_paid|floatformat:2 }} руб.</h4>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-3">
<div class="col-md-6"> <!-- Единая форма: Платёж / Возврат -->
<div class="card border-{% if order.amount_due > 0 %}warning{% else %}success{% endif %}"> <form method="post"
<div class="card-body text-center p-2"> action="{% url 'orders:transaction-add-payment' order.order_number %}"
<small class="text-muted d-block">Остаток</small> id="unified-transaction-form">
<h6 class="mb-0 text-{% if order.amount_due > 0 %}warning{% else %}success{% endif %}">{{ order.amount_due|floatformat:2 }} руб.</h6> {% csrf_token %}
</div>
<!-- Переключатель режима -->
<div class="mb-3">
<label class="form-label fw-bold d-block">Режим</label>
<div class="btn-group w-100" role="group" id="mode-toggle">
<button type="button" class="btn btn-outline-secondary" data-mode="payment">Платёж</button>
<button type="button" class="btn btn-outline-secondary"
data-mode="refund"
{% if order.amount_paid <= 0 %}disabled{% endif %}>
Возврат
</button>
</div> </div>
</div> </div>
{% if order.customer %}
<div class="col-md-6"> <!-- Способы (цикл по всем доступным) -->
<div class="card border-info"> <div class="mb-2">
<div class="card-body text-center p-2"> <label class="form-label fw-bold d-block">Способ</label>
<small class="text-muted d-block"><i class="bi bi-wallet2"></i> Кошелёк</small> <div class="d-flex flex-wrap gap-2" id="unified-methods">
<h6 class="mb-0 text-info">{{ order.customer.wallet_balance|floatformat:2 }} руб.</h6> {% load orders_tags %}
</div> {% get_payment_methods as payment_methods %}
{% for pm in payment_methods %}
<button type="button"
class="btn btn-outline-primary method-btn flex-fill"
data-id="{{ pm.id }}"
data-code="{{ pm.code }}">
{{ pm.name }}
{% if pm.code == 'account_balance' and order.customer %}
<span class="badge bg-info ms-1">
{{ order.customer.wallet_balance|floatformat:2 }} руб.
</span>
{% endif %}
</button>
{% endfor %}
</div> </div>
<!-- Один скрытый инпут: имя будет меняться в зависимости от режима -->
<input type="hidden" id="unified-method-id" required>
</div> </div>
{% endif %}
</div> <!-- Сумма (одно поле для обоих режимов) -->
{% endif %} <div class="mb-2">
<label class="form-label fw-bold">Сумма</label>
<div class="input-group">
<input type="number"
step="0.01"
min="0.01"
class="form-control"
id="unified-amount"
value="{{ order.amount_due|default:0|unlocalize }}"
required>
<span class="input-group-text">руб.</span>
</div>
<small class="text-muted" id="amount-hint" style="display:none;">
Макс: <span id="amount-max"></span> руб.
</small>
</div>
<!-- Причина (только для возврата) -->
<div class="mb-2" id="refund-reason-wrap" style="display:none;">
<label class="form-label fw-bold">Причина</label>
<input type="text" class="form-control" id="refund-reason" placeholder="Укажите причину возврата">
</div>
<!-- Примечание (одно поле для обоих режимов) -->
<div class="mb-3">
<label class="form-label fw-bold">Примечание</label>
<input type="text" class="form-control" id="unified-notes" placeholder="Опционально">
</div>
<button type="submit" class="btn w-100" id="unified-submit">
<i class="bi bi-check-lg"></i> Оплатить
</button>
</form>
<!-- История транзакций --> <!-- История транзакций -->
{% if order.pk and order.transactions.exists %} {% if order.transactions.exists %}
<div class="mb-3"> <div class="mt-4">
<h6 class="text-muted mb-2"><i class="bi bi-clock-history"></i> История транзакций</h6> <h6 class="text-muted mb-2"><i class="bi bi-clock-history"></i> История транзакций</h6>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-hover mb-0"> <table class="table table-sm table-hover mb-0">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th width="25%">Дата</th> <th width="25%">Дата</th>
<th width="30%">Способ оплаты</th> <th width="30%">Способ</th>
<th width="25%" class="text-end">Сумма</th> <th width="25%" class="text-end">Сумма</th>
<th width="20%">Кем</th> <th width="20%">Кем</th>
</tr> </tr>
@@ -712,131 +781,19 @@
</table> </table>
</div> </div>
</div> </div>
{% elif order.pk %} {% else %}
<div class="alert alert-light mb-3"> <div class="alert alert-light mt-4">
<i class="bi bi-info-circle"></i> Транзакции по заказу отсутствуют <i class="bi bi-info-circle"></i> Транзакции по заказу отсутствуют
</div> </div>
{% endif %} {% endif %}
{% else %}
</div> <!-- Для новых заказов -->
</div> <div class="alert alert-info">
<i class="bi bi-info-circle"></i> Сохраните заказ, чтобы добавить платежи и возвраты.
<!-- Формы добавления платежа и возврата -->
{% if order.pk %}
<div class="card mb-3">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="payment-tab-real" data-bs-toggle="tab" data-bs-target="#payment-form-real" type="button" role="tab">
<i class="bi bi-plus-circle"></i> Добавить платёж
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="refund-tab-real" data-bs-toggle="tab" data-bs-target="#refund-form-real" type="button" role="tab" {% if order.amount_paid <= 0 %}disabled{% endif %}>
<i class="bi bi-arrow-return-left"></i> Создать возврат
</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Форма добавления платежа -->
<div class="tab-pane fade show active" id="payment-form-real" role="tabpanel">
<form method="post" action="{% url 'orders:transaction-add-payment' order.order_number %}" id="payment-add-form">
{% csrf_token %}
<div class="row align-items-end">
<div class="col-12 mb-2">
<label class="form-label fw-bold">Способ оплаты <span class="text-danger">*</span></label>
<select name="payment_method" class="form-select" id="payment-method-select" required>
<option value="">Выберите способ...</option>
{% load orders_tags %}
{% get_payment_methods as payment_methods %}
{% for pm in payment_methods %}
<option value="{{ pm.id }}" data-code="{{ pm.code }}">{{ pm.name }}{% if pm.code == 'account_balance' %} (кошелёк клиента){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 mb-2">
<label class="form-label fw-bold">Сумма <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" name="amount" step="0.01" min="0.01"
class="form-control" placeholder="0.00" id="payment-amount-input"
{% if order.amount_due > 0 %}value="{{ order.amount_due|unlocalize }}"{% endif %} required>
<span class="input-group-text">руб.</span>
</div>
<small class="text-muted" id="payment-limit-hint" style="display:none;">Макс: <span id="payment-max-value"></span> руб.</small>
</div>
<div class="col-md-6 mb-2">
<label class="form-label fw-bold">Остаток</label>
<div class="form-control-plaintext fw-bold text-warning">
{{ order.amount_due|floatformat:2 }} руб.
</div>
</div>
<div class="col-12 mb-2">
<label class="form-label fw-bold">Примечания</label>
<input type="text" name="notes" class="form-control" placeholder="Опционально">
</div>
<div class="col-12">
<button type="submit" class="btn btn-success w-100">
<i class="bi bi-check-lg"></i> Добавить платёж
</button>
</div>
</div>
</form>
</div>
<!-- Форма создания возврата -->
<div class="tab-pane fade" id="refund-form-real" role="tabpanel">
<form method="post" action="{% url 'orders:transaction-add-refund' order.order_number %}" id="refund-add-form">
{% csrf_token %}
<div class="alert alert-warning mb-3">
<i class="bi bi-exclamation-triangle"></i>
<strong>Возврат средств клиенту</strong><br>
<small>Можно вернуть любую сумму до {{ order.amount_paid|floatformat:2 }} руб. Выберите способ возврата (обычно соответствует способу оплаты). Зачисление в кошелёк клиента произойдёт только при выборе метода «кошелёк клиента».</small>
</div>
<div class="row align-items-end">
<div class="col-12 mb-2">
<label class="form-label fw-bold">Способ возврата <span class="text-danger">*</span></label>
<select name="refund_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 }}{% if pm.code == 'account_balance' %} (кошелёк клиента){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-12 mb-2">
<label class="form-label fw-bold">Сумма возврата <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" name="refund_amount" step="0.01" min="0.01"
max="{{ order.amount_paid|unlocalize }}" class="form-control"
placeholder="0.00" required>
<span class="input-group-text">руб.</span>
</div>
<small class="text-muted">Макс: {{ order.amount_paid|floatformat:2 }}</small>
</div>
<div class="col-12 mb-2">
<label class="form-label fw-bold">Причина <span class="text-danger">*</span></label>
<input type="text" name="refund_reason" class="form-control"
placeholder="Укажите причину возврата" required>
</div>
<div class="col-12 mb-2">
<label class="form-label fw-bold">Примечания</label>
<input type="text" name="refund_notes" class="form-control" placeholder="Опционально">
</div>
<div class="col-12">
<button type="submit" class="btn btn-warning w-100 text-dark">
<i class="bi bi-arrow-return-left"></i> Вернуть средства
</button>
</div>
</div>
</form>
</div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<!-- Конец правой колонки --> <!-- Конец правой колонки -->
</div> </div>
@@ -909,32 +866,130 @@ document.addEventListener('DOMContentLoaded', function() {
</script> </script>
<script> <script>
// Ограничение оплаты из кошелька // Унифицированная форма платежа/возврата
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const paymentMethodSelect = document.getElementById('payment-method-select'); const form = document.getElementById('unified-transaction-form');
const paymentAmountInput = document.getElementById('payment-amount-input'); if (!form) return; // Форма есть только для существующих заказов
const paymentLimitHint = document.getElementById('payment-limit-hint');
const paymentMaxValue = document.getElementById('payment-max-value'); {% if order.pk %}
// URLs для смены action
if (paymentMethodSelect && paymentAmountInput) { const paymentUrl = "{% url 'orders:transaction-add-payment' order.order_number %}";
const amountDue = {{ order.amount_due|default:0|unlocalize }}; const refundUrl = "{% url 'orders:transaction-add-refund' order.order_number %}";
paymentMethodSelect.addEventListener('change', function() { // Элементы
const selectedOption = this.options[this.selectedIndex]; const modeButtons = document.querySelectorAll('#mode-toggle [data-mode]');
const paymentCode = selectedOption ? selectedOption.getAttribute('data-code') : null; const methodButtons = document.querySelectorAll('.method-btn');
const methodHidden = document.getElementById('unified-method-id');
if (paymentCode === 'account_balance') {
// Кошелёк: ограничиваем максимумом const amountInput = document.getElementById('unified-amount');
paymentAmountInput.setAttribute('max', amountDue); const amountHint = document.getElementById('amount-hint');
paymentMaxValue.textContent = amountDue.toFixed(2); const amountMaxSpan = document.getElementById('amount-max');
paymentLimitHint.style.display = 'block';
const refundReasonWrap = document.getElementById('refund-reason-wrap');
const refundReasonInput = document.getElementById('refund-reason');
const notesInput = document.getElementById('unified-notes');
const submitBtn = document.getElementById('unified-submit');
// Данные
const amountDue = {{ order.amount_due|default:0|unlocalize }};
const amountPaid = {{ order.amount_paid|default:0|unlocalize }};
const walletBalance = {% if order.customer %}{{ order.customer.wallet_balance|default:0|unlocalize }}{% else %}0{% endif %};
// Состояние
let currentMode = 'payment'; // режим по умолчанию — Платёж
let currentMethodCode = null;
// Применить режим
function applyMode() {
if (currentMode === 'payment') {
form.action = paymentUrl;
// Имена полей под платеж
methodHidden.name = 'payment_method';
amountInput.name = 'amount';
notesInput.name = 'notes';
refundReasonWrap.style.display = 'none';
refundReasonInput.removeAttribute('name');
refundReasonInput.removeAttribute('required');
// Стили кнопки
submitBtn.classList.remove('btn-warning', 'text-dark');
submitBtn.classList.add('btn-success');
submitBtn.innerHTML = '<i class="bi bi-check-lg"></i> Оплатить';
// Значение суммы по умолчанию — неоплаченная сумма
amountInput.value = amountDue || '';
// Лимиты: только если выбран кошелёк
updateLimitsForPayment();
} else {
form.action = refundUrl;
// Имена полей под возврат
methodHidden.name = 'refund_payment_method';
amountInput.name = 'refund_amount';
notesInput.name = 'refund_notes';
refundReasonWrap.style.display = 'block';
refundReasonInput.name = 'refund_reason';
refundReasonInput.setAttribute('required', 'required');
// Стили кнопки
submitBtn.classList.remove('btn-success');
submitBtn.classList.add('btn-warning', 'text-dark');
submitBtn.innerHTML = '<i class="bi bi-arrow-return-left"></i> Вернуть средства';
// Значение суммы — пусто, лимит по оплаченным средствам
amountInput.value = '';
updateLimitsForRefund();
}
}
// Лимиты для платежа (кошелёк ограничивается балансом и суммой к оплате)
function updateLimitsForPayment() {
if (currentMethodCode === 'account_balance') {
const max = Math.min(amountDue, walletBalance);
amountInput.max = max;
amountHint.style.display = 'inline';
amountMaxSpan.textContent = (max || 0).toFixed(2);
} else {
amountInput.removeAttribute('max');
amountHint.style.display = 'none';
}
}
// Лимиты для возврата (всегда ограничиваем суммой оплаченного)
function updateLimitsForRefund() {
amountInput.max = amountPaid || 0;
amountHint.style.display = 'inline';
amountMaxSpan.textContent = (amountPaid || 0).toFixed(2);
}
// Выбор режима
modeButtons.forEach(btn => {
btn.addEventListener('click', () => {
modeButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentMode = btn.dataset.mode;
applyMode();
});
});
// Выбор способа (не активируем по умолчанию)
methodButtons.forEach(btn => {
btn.addEventListener('click', () => {
methodButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
methodHidden.value = btn.dataset.id;
currentMethodCode = btn.dataset.code;
if (currentMode === 'payment') {
updateLimitsForPayment();
} else { } else {
// Внешние методы: снимаем ограничение updateLimitsForRefund();
paymentAmountInput.removeAttribute('max');
paymentLimitHint.style.display = 'none';
} }
}); });
} });
// Инициализация начального режима (без авто-выбора способа)
applyMode();
{% endif %}
}); });
</script> </script>