feat: Add customer prefill from URL parameter in order creation

- Modified order_create view to read customer from GET parameter
- Pass preselected_customer to template context
- Template renders select with preselected option for Select2
- Fixed draft creation timing with callback after Select2 initialization
- Auto-create draft when customer is preselected from URL
- Graceful handling if customer not found or invalid ID
This commit is contained in:
2025-11-27 00:17:02 +03:00
parent 5ead7fdd2e
commit c62cdb0298
6 changed files with 463 additions and 19 deletions

View File

@@ -126,7 +126,15 @@
<label for="{{ form.customer.id_for_label }}" class="form-label">
Клиент <span class="text-danger">*</span>
</label>
{{ form.customer }}
{% if preselected_customer %}
<select name="customer" class="form-select" id="id_customer">
<option value="{{ preselected_customer.pk }}" selected data-name="{{ preselected_customer.name }}" data-phone="{{ preselected_customer.phone|default:'' }}" data-email="{{ preselected_customer.email|default:'' }}">
{{ preselected_customer.name }}{% if preselected_customer.phone %} ({{ preselected_customer.phone }}){% endif %}
</option>
</select>
{% else %}
{{ form.customer }}
{% endif %}
{% if form.customer.errors %}
<div class="text-danger">{{ form.customer.errors }}</div>
{% endif %}
@@ -565,11 +573,43 @@
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Оплата</h5>
<button type="button" class="btn btn-sm btn-success" id="add-payment-btn">
<i class="bi bi-plus-circle"></i> Добавить платеж
</button>
<div>
<span class="badge bg-{% if order.payment_status == 'paid' %}success{% elif order.payment_status == 'partial' %}warning{% else %}danger{% endif %} me-2">
{{ order.get_payment_status_display }}
</span>
<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">
<!-- Блок кошелька клиента -->
{% if order.customer %}
<div class="alert alert-info d-flex justify-content-between align-items-center">
<div>
<strong>Кошелёк клиента:</strong>
{% if order.customer.wallet_balance > 0 %}
<span class="text-success fw-bold">{{ order.customer.wallet_balance|floatformat:2 }} руб.</span>
{% else %}
<span class="text-muted">0.00 руб.</span>
{% endif %}
<span class="ms-3">Остаток к оплате: <strong>{{ order.amount_due|floatformat:2 }} руб.</strong></span>
</div>
{% if order.customer.wallet_balance > 0 and order.amount_due > 0 %}
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" id="apply-wallet-max-btn">
Учесть максимум
</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>
{% endif %}
</div>
{% endif %}
<!-- Скрытые поля для formset management -->
{{ payment_formset.management_form }}
@@ -639,6 +679,9 @@
<label class="form-label">Способ оплаты</label>
<select name="payments-__prefix__-payment_method" class="form-select" id="id_payments-__prefix__-payment_method">
<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>
@@ -675,6 +718,98 @@
</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;
}
// Найти существующую форму платежа без метода, иначе добавить новую
let formEl = document.querySelector('#payments-container .payment-form:last-child');
if (!formEl) {
formEl = addPaymentRow();
}
const sel = formEl.querySelector('select[id^="id_payments-"][id$="-payment_method"]');
const amt = formEl.querySelector('input[id^="id_payments-"][id$="-amount"]');
selectAccountBalance(sel);
if (amt) {
amt.value = amount.toFixed(2);
amt.setAttribute('max', Math.min(walletBalance, amountDue).toFixed(2));
}
}
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>
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Дополнительно</h5>
@@ -781,6 +916,14 @@ function initCustomerSelect2() {
console.log('Значение восстановлено:', $customerSelect.val());
}
// Уведомляем draft-creator.js что Select2 готов и есть предзаполненное значение
if (currentValue && window.DraftCreator) {
console.log('7. Уведомляем DraftCreator о предзаполненном клиенте');
setTimeout(function() {
window.DraftCreator.triggerDraftCreation();
}, 100);
}
// Слушаем события
$customerSelect.on('select2:open', function(e) {
console.log('7. Dropdown открыт');