Упрощена форма оплаты заказа: единая форма платежа/возврата с переключателем режимов
This commit is contained in:
@@ -595,21 +595,24 @@
|
||||
|
||||
<!-- Правая колонка: Оплата -->
|
||||
<div class="col-lg-5">
|
||||
<!-- Оплата и возвраты -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5>
|
||||
{% if order.pk and order.amount_paid > 0 %}
|
||||
<span class="badge bg-secondary">Доступно для возврата: {{ order.amount_paid|floatformat:2 }} руб.</span>
|
||||
{% if order.pk %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Блок статистики -->
|
||||
{% if order.pk %}
|
||||
|
||||
<!-- Предупреждение о переплате -->
|
||||
{% if order.amount_paid > order.total_amount %}
|
||||
<div class="alert alert-warning mb-3">
|
||||
@@ -618,61 +621,127 @@
|
||||
Оплачено <strong>{{ order.amount_paid|floatformat:2 }} руб.</strong>,
|
||||
сумма заказа <strong>{{ order.total_amount|floatformat:2 }} руб.</strong><br>
|
||||
Переплата: <strong class="text-danger">{{ order.overpayment|floatformat:2 }} руб.</strong><br>
|
||||
Создайте возврат в кошелёк клиента во вкладке «Создать возврат» ниже.
|
||||
Создайте возврат средств ниже.
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-primary">
|
||||
<!-- Сумма заказа и остаток -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center p-2">
|
||||
<small class="text-muted d-block">Сумма заказа</small>
|
||||
<h6 class="mb-0 text-primary">{{ order.total_amount|floatformat:2 }} руб.</h6>
|
||||
<small class="text-muted d-block mb-1">Сумма заказа</small>
|
||||
<h4 class="text-primary mb-0">{{ order.total_amount|floatformat:2 }} руб.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card border-success">
|
||||
<div class="col-4">
|
||||
<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">
|
||||
<small class="text-muted d-block">Оплачено</small>
|
||||
<h6 class="mb-0 text-success">{{ order.amount_paid|floatformat:2 }} руб.</h6>
|
||||
<small class="text-muted d-block mb-1">Остаток к оплате</small>
|
||||
<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>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-{% if order.amount_due > 0 %}warning{% else %}success{% endif %}">
|
||||
<div class="col-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center p-2">
|
||||
<small class="text-muted d-block">Остаток</small>
|
||||
<h6 class="mb-0 text-{% if order.amount_due > 0 %}warning{% else %}success{% endif %}">{{ order.amount_due|floatformat:2 }} руб.</h6>
|
||||
<small class="text-muted d-block mb-1">Уже оплачено</small>
|
||||
<h4 class="text-success mb-0">{{ order.amount_paid|floatformat:2 }} руб.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if order.customer %}
|
||||
<div class="col-md-6">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center p-2">
|
||||
<small class="text-muted d-block"><i class="bi bi-wallet2"></i> Кошелёк</small>
|
||||
<h6 class="mb-0 text-info">{{ order.customer.wallet_balance|floatformat:2 }} руб.</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Единая форма: Платёж / Возврат -->
|
||||
<form method="post"
|
||||
action="{% url 'orders:transaction-add-payment' order.order_number %}"
|
||||
id="unified-transaction-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Переключатель режима -->
|
||||
<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 class="mb-2">
|
||||
<label class="form-label fw-bold d-block">Способ</label>
|
||||
<div class="d-flex flex-wrap gap-2" id="unified-methods">
|
||||
{% load orders_tags %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<!-- Один скрытый инпут: имя будет меняться в зависимости от режима -->
|
||||
<input type="hidden" id="unified-method-id" required>
|
||||
</div>
|
||||
|
||||
<!-- Сумма (одно поле для обоих режимов) -->
|
||||
<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 %}
|
||||
<div class="mb-3">
|
||||
{% if order.transactions.exists %}
|
||||
<div class="mt-4">
|
||||
<h6 class="text-muted mb-2"><i class="bi bi-clock-history"></i> История транзакций</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th width="25%">Дата</th>
|
||||
<th width="30%">Способ оплаты</th>
|
||||
<th width="30%">Способ</th>
|
||||
<th width="25%" class="text-end">Сумма</th>
|
||||
<th width="20%">Кем</th>
|
||||
</tr>
|
||||
@@ -712,132 +781,20 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% elif order.pk %}
|
||||
<div class="alert alert-light mb-3">
|
||||
{% else %}
|
||||
<div class="alert alert-light mt-4">
|
||||
<i class="bi bi-info-circle"></i> Транзакции по заказу отсутствуют
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Формы добавления платежа и возврата -->
|
||||
{% 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>
|
||||
{% else %}
|
||||
<!-- Для новых заказов -->
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Сохраните заказ, чтобы добавить платежи и возвраты.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Конец правой колонки -->
|
||||
</div>
|
||||
<!-- Конец двух колонок -->
|
||||
@@ -909,32 +866,130 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Ограничение оплаты из кошелька
|
||||
// Унифицированная форма платежа/возврата
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const paymentMethodSelect = document.getElementById('payment-method-select');
|
||||
const paymentAmountInput = document.getElementById('payment-amount-input');
|
||||
const paymentLimitHint = document.getElementById('payment-limit-hint');
|
||||
const paymentMaxValue = document.getElementById('payment-max-value');
|
||||
const form = document.getElementById('unified-transaction-form');
|
||||
if (!form) return; // Форма есть только для существующих заказов
|
||||
|
||||
if (paymentMethodSelect && paymentAmountInput) {
|
||||
{% if order.pk %}
|
||||
// URLs для смены action
|
||||
const paymentUrl = "{% url 'orders:transaction-add-payment' order.order_number %}";
|
||||
const refundUrl = "{% url 'orders:transaction-add-refund' order.order_number %}";
|
||||
|
||||
// Элементы
|
||||
const modeButtons = document.querySelectorAll('#mode-toggle [data-mode]');
|
||||
const methodButtons = document.querySelectorAll('.method-btn');
|
||||
const methodHidden = document.getElementById('unified-method-id');
|
||||
|
||||
const amountInput = document.getElementById('unified-amount');
|
||||
const amountHint = document.getElementById('amount-hint');
|
||||
const amountMaxSpan = document.getElementById('amount-max');
|
||||
|
||||
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 %};
|
||||
|
||||
paymentMethodSelect.addEventListener('change', function() {
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
const paymentCode = selectedOption ? selectedOption.getAttribute('data-code') : null;
|
||||
// Состояние
|
||||
let currentMode = 'payment'; // режим по умолчанию — Платёж
|
||||
let currentMethodCode = null;
|
||||
|
||||
if (paymentCode === 'account_balance') {
|
||||
// Кошелёк: ограничиваем максимумом
|
||||
paymentAmountInput.setAttribute('max', amountDue);
|
||||
paymentMaxValue.textContent = amountDue.toFixed(2);
|
||||
paymentLimitHint.style.display = 'block';
|
||||
// Применить режим
|
||||
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 {
|
||||
// Внешние методы: снимаем ограничение
|
||||
paymentAmountInput.removeAttribute('max');
|
||||
paymentLimitHint.style.display = 'none';
|
||||
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 {
|
||||
updateLimitsForRefund();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация начального режима (без авто-выбора способа)
|
||||
applyMode();
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user