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:
@@ -5,6 +5,7 @@ from .models import Order, OrderItem, Payment, Address, OrderStatus
|
||||
from customers.models import Customer
|
||||
from inventory.models import Warehouse
|
||||
from products.models import Product, ProductKit
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class OrderForm(forms.ModelForm):
|
||||
@@ -481,6 +482,44 @@ class PaymentForm(forms.ModelForm):
|
||||
# Делаем notes опциональным
|
||||
self.fields['notes'].required = False
|
||||
|
||||
def clean(self):
|
||||
"""Валидация платежа, особенно для оплаты из кошелька"""
|
||||
cleaned = super().clean()
|
||||
method = cleaned.get('payment_method')
|
||||
amount = cleaned.get('amount')
|
||||
order = getattr(self.instance, 'order', None)
|
||||
|
||||
# Пустые формы допустимы при удалении
|
||||
if not method and not amount:
|
||||
return cleaned
|
||||
|
||||
# Базовые проверки
|
||||
if amount is None or amount <= 0:
|
||||
self.add_error('amount', 'Введите сумму больше 0.')
|
||||
|
||||
if not order:
|
||||
raise forms.ValidationError('Заказ не найден для платежа.')
|
||||
|
||||
# Проверка для оплаты из кошелька
|
||||
if method and getattr(method, 'code', None) == 'account_balance':
|
||||
wallet_balance = order.customer.wallet_balance if order.customer else Decimal('0')
|
||||
amount_due = max(order.total_amount - order.amount_paid, Decimal('0'))
|
||||
|
||||
if wallet_balance <= 0:
|
||||
self.add_error('payment_method', 'Недостаточно средств в кошельке клиента (баланс 0).')
|
||||
|
||||
if amount and amount > wallet_balance:
|
||||
self.add_error('amount', f'Недостаточно средств в кошельке. Доступно {wallet_balance} руб.')
|
||||
|
||||
if amount and amount > amount_due:
|
||||
self.add_error('amount', f'Сумма превышает остаток к оплате ({amount_due} руб.)')
|
||||
|
||||
if self.errors:
|
||||
# Общее сообщение для блока формы
|
||||
raise forms.ValidationError('Проверьте поля оплаты из кошелька.')
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
# Formset для множественных платежей
|
||||
PaymentFormSet = inlineformset_factory(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.db import models
|
||||
from accounts.models import CustomUser
|
||||
from decimal import Decimal
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class PaymentMethod(models.Model):
|
||||
@@ -137,16 +140,41 @@ class Payment(models.Model):
|
||||
return f"Платеж {self.amount} руб. по заказу #{self.order.order_number}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""При сохранении платежа обновляем сумму оплаты в заказе"""
|
||||
super().save(*args, **kwargs)
|
||||
# Пересчитываем общую сумму оплаты в заказе
|
||||
self.order.amount_paid = sum(p.amount for p in self.order.payments.all())
|
||||
self.order.update_payment_status()
|
||||
"""При сохранении платежа обновляем сумму оплаты в заказе и обрабатываем кошелёк/переплаты"""
|
||||
is_new = self.pk is None
|
||||
with transaction.atomic():
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Нормализация переплаты: лишнее в кошелёк, amount_paid = total_amount
|
||||
try:
|
||||
from customers.services.wallet_service import WalletService
|
||||
WalletService.add_overpayment(self.order, self.created_by)
|
||||
except Exception:
|
||||
# Если обработка переплаты не удалась, продолжаем без ошибок
|
||||
pass
|
||||
# Пересчитываем общую сумму оплаты в заказе
|
||||
self.order.amount_paid = sum(p.amount for p in self.order.payments.all())
|
||||
self.order.update_payment_status()
|
||||
|
||||
# Списание из кошелька при новом платеже методом 'account_balance'
|
||||
if is_new and self.payment_method.code == 'account_balance':
|
||||
from customers.models import Customer, WalletTransaction
|
||||
# Блокируем запись клиента
|
||||
customer = Customer.objects.select_for_update().get(pk=self.order.customer_id)
|
||||
if customer.wallet_balance < self.amount:
|
||||
raise ValidationError(f'Недостаточно средств в кошельке (доступно {customer.wallet_balance} руб.)')
|
||||
|
||||
# Списываем и округляем до 2 знаков
|
||||
customer.wallet_balance = (customer.wallet_balance - self.amount).quantize(Decimal('0.01'))
|
||||
customer.save(update_fields=['wallet_balance'])
|
||||
|
||||
# Пишем историю
|
||||
WalletTransaction.objects.create(
|
||||
customer=customer,
|
||||
amount=self.amount,
|
||||
transaction_type='spend',
|
||||
order=self.order,
|
||||
description=f'Оплата из кошелька по заказу #{self.order.order_number}',
|
||||
created_by=self.created_by
|
||||
)
|
||||
|
||||
# Нормализация переплаты: лишнее в кошелёк, amount_paid = total_amount
|
||||
try:
|
||||
from customers.services.wallet_service import WalletService
|
||||
WalletService.add_overpayment(self.order, self.created_by)
|
||||
except Exception:
|
||||
# Продолжаем, даже если нормализация переплаты не удалась
|
||||
pass
|
||||
|
||||
@@ -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 открыт');
|
||||
|
||||
@@ -108,7 +108,19 @@ def order_create(request):
|
||||
else:
|
||||
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||
else:
|
||||
form = OrderForm()
|
||||
# Предзаполнение клиента из GET параметра
|
||||
initial_data = {}
|
||||
preselected_customer = None
|
||||
customer_id = request.GET.get('customer')
|
||||
if customer_id:
|
||||
try:
|
||||
from customers.models import Customer
|
||||
preselected_customer = Customer.objects.get(pk=customer_id)
|
||||
initial_data['customer'] = preselected_customer.pk
|
||||
except (Customer.DoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
form = OrderForm(initial=initial_data)
|
||||
formset = OrderItemFormSet()
|
||||
payment_formset = PaymentFormSet()
|
||||
|
||||
@@ -116,6 +128,7 @@ def order_create(request):
|
||||
'form': form,
|
||||
'formset': formset,
|
||||
'payment_formset': payment_formset,
|
||||
'preselected_customer': preselected_customer,
|
||||
'title': 'Создание заказа',
|
||||
'button_text': 'Создать заказ',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user