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:
@@ -46,9 +46,11 @@
|
|||||||
<th>Баланс кошелька:</th>
|
<th>Баланс кошелька:</th>
|
||||||
<td>
|
<td>
|
||||||
{% if customer.wallet_balance > 0 %}
|
{% if customer.wallet_balance > 0 %}
|
||||||
<span class="text-success fw-bold">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
<span class="badge bg-success" style="font-size: 1.1em;">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||||
|
{% elif customer.wallet_balance == 0 %}
|
||||||
|
<span class="badge bg-secondary">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ customer.wallet_balance|floatformat:2 }} руб.
|
<span class="badge bg-danger">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -79,6 +81,211 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- История транзакций кошелька -->
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5>История кошелька (последние 20)</h5>
|
||||||
|
<span class="badge bg-primary">{{ wallet_transactions|length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if wallet_transactions %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Тип</th>
|
||||||
|
<th>Сумма</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th>Заказ</th>
|
||||||
|
<th>Создал</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for transaction in wallet_transactions %}
|
||||||
|
<tr>
|
||||||
|
<td><small>{{ transaction.created_at|date:"d.m.Y H:i" }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if transaction.transaction_type == 'deposit' %}
|
||||||
|
<span class="badge bg-success">Пополнение</span>
|
||||||
|
{% elif transaction.transaction_type == 'spend' %}
|
||||||
|
<span class="badge bg-danger">Списание</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">Корректировка</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if transaction.transaction_type == 'deposit' or transaction.transaction_type == 'adjustment' and transaction.amount > 0 %}
|
||||||
|
<span class="text-success fw-bold">+{{ transaction.amount|floatformat:2 }} руб.</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger fw-bold">-{{ transaction.amount|floatformat:2 }} руб.</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ transaction.description|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if transaction.order %}
|
||||||
|
<a href="{% url 'orders:order-detail' transaction.order.pk %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
#{{ transaction.order.order_number }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><small>{{ transaction.created_by.username|default:"-" }}</small></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">История транзакций пуста.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- История заказов -->
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5>История заказов</h5>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-primary">{{ orders_page.paginator.count }}</span>
|
||||||
|
<a href="{% url 'orders:order-create' %}?customer={{ customer.pk }}" class="btn btn-sm btn-success ms-2">
|
||||||
|
<i class="bi bi-plus-circle"></i> Новый заказ
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if orders_page %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>№</th>
|
||||||
|
<th>Дата создания</th>
|
||||||
|
<th>Дата доставки</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Оплата</th>
|
||||||
|
<th>Сумма</th>
|
||||||
|
<th>Оплачено</th>
|
||||||
|
<th>Остаток</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in orders_page %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>#{{ order.order_number }}</strong></td>
|
||||||
|
<td><small>{{ order.created_at|date:"d.m.Y H:i" }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if order.delivery_date %}
|
||||||
|
<strong>{{ order.delivery_date|date:"d.m.Y" }}</strong>
|
||||||
|
{% if order.delivery_time %}
|
||||||
|
<br><small class="text-muted">{{ order.delivery_time }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if order.is_delivery %}
|
||||||
|
<br><span class="badge bg-info">Доставка</span>
|
||||||
|
{% else %}
|
||||||
|
<br><span class="badge bg-secondary">Самовывоз</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Не указана</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if order.status == 'draft' %}
|
||||||
|
<span class="badge bg-secondary">Черновик</span>
|
||||||
|
{% elif order.status == 'pending' %}
|
||||||
|
<span class="badge bg-warning">Ожидает</span>
|
||||||
|
{% elif order.status == 'in_production' %}
|
||||||
|
<span class="badge bg-info">В производстве</span>
|
||||||
|
{% elif order.status == 'ready' %}
|
||||||
|
<span class="badge bg-primary">Готов</span>
|
||||||
|
{% elif order.status == 'delivered' %}
|
||||||
|
<span class="badge bg-success">Доставлен</span>
|
||||||
|
{% elif order.status == 'cancelled' %}
|
||||||
|
<span class="badge bg-danger">Отменён</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ order.get_status_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if order.payment_status == 'paid' %}
|
||||||
|
<span class="badge bg-success">Оплачено</span>
|
||||||
|
{% elif order.payment_status == 'partial' %}
|
||||||
|
<span class="badge bg-warning">Частично</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Не оплачено</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><strong>{{ order.total_amount|floatformat:2 }} руб.</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if order.amount_paid > 0 %}
|
||||||
|
<span class="text-success">{{ order.amount_paid|floatformat:2 }} руб.</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">0.00 руб.</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if order.amount_due > 0 %}
|
||||||
|
<span class="text-danger fw-bold">{{ order.amount_due|floatformat:2 }} руб.</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-success">0.00 руб.</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'orders:order-detail' order.pk %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'orders:order-update' order.pk %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Пагинация -->
|
||||||
|
{% if orders_page.has_other_pages %}
|
||||||
|
<nav aria-label="Навигация по заказам">
|
||||||
|
<ul class="pagination justify-content-center mt-3">
|
||||||
|
{% if orders_page.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page=1">Первая</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ orders_page.previous_page_number }}">Предыдущая</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">
|
||||||
|
Страница {{ orders_page.number }} из {{ orders_page.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if orders_page.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ orders_page.next_page_number }}">Следующая</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ orders_page.paginator.num_pages }}">Последняя</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">У клиента пока нет заказов.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -89,10 +89,24 @@ def customer_detail(request, pk):
|
|||||||
active_orders = customer.orders.exclude(payment_status='paid')
|
active_orders = customer.orders.exclude(payment_status='paid')
|
||||||
total_debt = sum(order.amount_due for order in active_orders)
|
total_debt = sum(order.amount_due for order in active_orders)
|
||||||
|
|
||||||
|
# История транзакций кошелька (последние 20)
|
||||||
|
from .models import WalletTransaction
|
||||||
|
wallet_transactions = WalletTransaction.objects.filter(
|
||||||
|
customer=customer
|
||||||
|
).select_related('order', 'created_by').order_by('-created_at')[:20]
|
||||||
|
|
||||||
|
# История заказов с пагинацией
|
||||||
|
orders_list = customer.orders.all().order_by('-created_at')
|
||||||
|
paginator = Paginator(orders_list, 10) # 10 заказов на страницу
|
||||||
|
page_number = request.GET.get('page')
|
||||||
|
orders_page = paginator.get_page(page_number)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'customer': customer,
|
'customer': customer,
|
||||||
'total_debt': total_debt,
|
'total_debt': total_debt,
|
||||||
'active_orders_count': active_orders.count(),
|
'active_orders_count': active_orders.count(),
|
||||||
|
'wallet_transactions': wallet_transactions,
|
||||||
|
'orders_page': orders_page,
|
||||||
}
|
}
|
||||||
return render(request, 'customers/customer_detail.html', context)
|
return render(request, 'customers/customer_detail.html', context)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from .models import Order, OrderItem, Payment, Address, OrderStatus
|
|||||||
from customers.models import Customer
|
from customers.models import Customer
|
||||||
from inventory.models import Warehouse
|
from inventory.models import Warehouse
|
||||||
from products.models import Product, ProductKit
|
from products.models import Product, ProductKit
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class OrderForm(forms.ModelForm):
|
class OrderForm(forms.ModelForm):
|
||||||
@@ -481,6 +482,44 @@ class PaymentForm(forms.ModelForm):
|
|||||||
# Делаем notes опциональным
|
# Делаем notes опциональным
|
||||||
self.fields['notes'].required = False
|
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 для множественных платежей
|
# Formset для множественных платежей
|
||||||
PaymentFormSet = inlineformset_factory(
|
PaymentFormSet = inlineformset_factory(
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import transaction
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethod(models.Model):
|
class PaymentMethod(models.Model):
|
||||||
@@ -137,16 +140,41 @@ class Payment(models.Model):
|
|||||||
return f"Платеж {self.amount} руб. по заказу #{self.order.order_number}"
|
return f"Платеж {self.amount} руб. по заказу #{self.order.order_number}"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""При сохранении платежа обновляем сумму оплаты в заказе"""
|
"""При сохранении платежа обновляем сумму оплаты в заказе и обрабатываем кошелёк/переплаты"""
|
||||||
super().save(*args, **kwargs)
|
is_new = self.pk is None
|
||||||
# Пересчитываем общую сумму оплаты в заказе
|
with transaction.atomic():
|
||||||
self.order.amount_paid = sum(p.amount for p in self.order.payments.all())
|
super().save(*args, **kwargs)
|
||||||
self.order.update_payment_status()
|
|
||||||
|
|
||||||
# Нормализация переплаты: лишнее в кошелёк, amount_paid = total_amount
|
# Пересчитываем общую сумму оплаты в заказе
|
||||||
try:
|
self.order.amount_paid = sum(p.amount for p in self.order.payments.all())
|
||||||
from customers.services.wallet_service import WalletService
|
self.order.update_payment_status()
|
||||||
WalletService.add_overpayment(self.order, self.created_by)
|
|
||||||
except Exception:
|
# Списание из кошелька при новом платеже методом 'account_balance'
|
||||||
# Если обработка переплаты не удалась, продолжаем без ошибок
|
if is_new and self.payment_method.code == 'account_balance':
|
||||||
pass
|
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">
|
<label for="{{ form.customer.id_for_label }}" class="form-label">
|
||||||
Клиент <span class="text-danger">*</span>
|
Клиент <span class="text-danger">*</span>
|
||||||
</label>
|
</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 %}
|
{% if form.customer.errors %}
|
||||||
<div class="text-danger">{{ form.customer.errors }}</div>
|
<div class="text-danger">{{ form.customer.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -565,11 +573,43 @@
|
|||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">Оплата</h5>
|
<h5 class="mb-0">Оплата</h5>
|
||||||
<button type="button" class="btn btn-sm btn-success" id="add-payment-btn">
|
<div>
|
||||||
<i class="bi bi-plus-circle"></i> Добавить платеж
|
<span class="badge bg-{% if order.payment_status == 'paid' %}success{% elif order.payment_status == 'partial' %}warning{% else %}danger{% endif %} me-2">
|
||||||
</button>
|
{{ 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>
|
||||||
<div class="card-body">
|
<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 -->
|
<!-- Скрытые поля для formset management -->
|
||||||
{{ payment_formset.management_form }}
|
{{ payment_formset.management_form }}
|
||||||
|
|
||||||
@@ -639,6 +679,9 @@
|
|||||||
<label class="form-label">Способ оплаты</label>
|
<label class="form-label">Способ оплаты</label>
|
||||||
<select name="payments-__prefix__-payment_method" class="form-select" id="id_payments-__prefix__-payment_method">
|
<select name="payments-__prefix__-payment_method" class="form-select" id="id_payments-__prefix__-payment_method">
|
||||||
<option value="">---------</option>
|
<option value="">---------</option>
|
||||||
|
{% for pm in payment_formset.forms.0.fields.payment_method.queryset %}
|
||||||
|
<option value="{{ pm.id }}">{{ pm.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -675,6 +718,98 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Дополнительно</h5>
|
<h5 class="mb-0">Дополнительно</h5>
|
||||||
@@ -781,6 +916,14 @@ function initCustomerSelect2() {
|
|||||||
console.log('Значение восстановлено:', $customerSelect.val());
|
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) {
|
$customerSelect.on('select2:open', function(e) {
|
||||||
console.log('7. Dropdown открыт');
|
console.log('7. Dropdown открыт');
|
||||||
|
|||||||
@@ -108,7 +108,19 @@ def order_create(request):
|
|||||||
else:
|
else:
|
||||||
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
|
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||||
else:
|
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()
|
formset = OrderItemFormSet()
|
||||||
payment_formset = PaymentFormSet()
|
payment_formset = PaymentFormSet()
|
||||||
|
|
||||||
@@ -116,6 +128,7 @@ def order_create(request):
|
|||||||
'form': form,
|
'form': form,
|
||||||
'formset': formset,
|
'formset': formset,
|
||||||
'payment_formset': payment_formset,
|
'payment_formset': payment_formset,
|
||||||
|
'preselected_customer': preselected_customer,
|
||||||
'title': 'Создание заказа',
|
'title': 'Создание заказа',
|
||||||
'button_text': 'Создать заказ',
|
'button_text': 'Создать заказ',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user