Добавлена возможность выбора анонимного системного клиента в форме заказа
- Убрана фильтрация системного клиента из результатов поиска (api_search_customers) - Добавлен флаг is_system_customer в результаты API поиска - Создан новый API endpoint api_get_system_customer для быстрого получения системного клиента - Добавлена кнопка 'Аноним' для быстрого выбора системного клиента - Системный клиент выделяется жёлтым цветом и иконкой инкогнито в выпадающем списке - Улучшена компактность результатов поиска (уменьшен шрифт до 13px) - Изменены пропорции полей: клиент 9 колонок, статус 3 колонки (было 6:6) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,5 +21,6 @@ urlpatterns = [
|
|||||||
# AJAX API endpoints
|
# AJAX API endpoints
|
||||||
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
||||||
path('api/create/', views.api_create_customer, name='api-create-customer'),
|
path('api/create/', views.api_create_customer, name='api-create-customer'),
|
||||||
|
path('api/system/', views.api_get_system_customer, name='api-get-system-customer'),
|
||||||
path('<int:pk>/api/update/', views.api_update_customer, name='api-update-customer'),
|
path('<int:pk>/api/update/', views.api_update_customer, name='api-update-customer'),
|
||||||
]
|
]
|
||||||
@@ -376,6 +376,39 @@ def build_customer_search_query(query, strategy, search_value):
|
|||||||
return Q(name__icontains=query)
|
return Q(name__icontains=query)
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def api_get_system_customer(request):
|
||||||
|
"""
|
||||||
|
AJAX endpoint для получения системного (анонимного) клиента.
|
||||||
|
|
||||||
|
Возвращает JSON с данными системного клиента:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"customer": {
|
||||||
|
"id": 1,
|
||||||
|
"text": "АНОНИМНЫЙ ПОКУПАТЕЛЬ (POS)",
|
||||||
|
"name": "АНОНИМНЫЙ ПОКУПАТЕЛЬ (POS)",
|
||||||
|
"phone": "",
|
||||||
|
"email": "system@pos.customer",
|
||||||
|
"is_system_customer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
system_customer, _ = Customer.get_or_create_system_customer()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'customer': {
|
||||||
|
'id': system_customer.pk,
|
||||||
|
'text': system_customer.name,
|
||||||
|
'name': system_customer.name,
|
||||||
|
'phone': str(system_customer.phone) if system_customer.phone else '',
|
||||||
|
'email': system_customer.email,
|
||||||
|
'is_system_customer': True,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def api_search_customers(request):
|
def api_search_customers(request):
|
||||||
"""
|
"""
|
||||||
@@ -456,8 +489,8 @@ def api_search_customers(request):
|
|||||||
if channel_matches:
|
if channel_matches:
|
||||||
q_objects |= Q(pk__in=channel_matches)
|
q_objects |= Q(pk__in=channel_matches)
|
||||||
|
|
||||||
# Исключаем системного клиента из результатов поиска
|
# Включаем всех клиентов, включая системного (для возможности выбора в заказах)
|
||||||
customers = Customer.objects.filter(q_objects).filter(is_system_customer=False).distinct().order_by('name')[:20]
|
customers = Customer.objects.filter(q_objects).distinct().order_by('name')[:20]
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@@ -475,6 +508,7 @@ def api_search_customers(request):
|
|||||||
'phone': phone_display,
|
'phone': phone_display,
|
||||||
'email': customer.email,
|
'email': customer.email,
|
||||||
'wallet_balance': float(customer.wallet_balance),
|
'wallet_balance': float(customer.wallet_balance),
|
||||||
|
'is_system_customer': customer.is_system_customer,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Если ничего не найдено, предлагаем создать нового клиента
|
# Если ничего не найдено, предлагаем создать нового клиента
|
||||||
|
|||||||
@@ -39,6 +39,14 @@
|
|||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Системный (анонимный) клиент - выделяем стилем
|
||||||
|
if (option.is_system_customer) {
|
||||||
|
return '<div class="customer-option system-customer-option">' +
|
||||||
|
'<i class="bi bi-incognito"></i> <strong class="text-warning">' + option.name + '</strong>' +
|
||||||
|
'<br><small class="text-muted">Системный клиент для анонимных покупок</small>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
if (!option.id) {
|
if (!option.id) {
|
||||||
return option.text;
|
return option.text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,22 @@
|
|||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для системного клиента */
|
||||||
|
.system-customer-option {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-customer-option:hover {
|
||||||
|
background-color: #ffe69c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-customer-option i {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Select2 dropdown styling */
|
/* Select2 dropdown styling */
|
||||||
.select2-results__option.customer-option-item {
|
.select2-results__option.customer-option-item {
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
@@ -55,6 +71,50 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Компактные стили для результатов поиска клиентов */
|
||||||
|
.customer-option {
|
||||||
|
padding: 4px 0 !important;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-option small {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-create-option {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-customer-option {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-customer-option small {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input group с Select2 и кнопкой системного клиента */
|
||||||
|
.customer-select-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-select-wrapper .select2-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-select-wrapper .btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ИСПРАВЛЕНИЕ: Убедимся что Select2 dropdown видим и поверх всех элементов */
|
/* ИСПРАВЛЕНИЕ: Убедимся что Select2 dropdown видим и поверх всех элементов */
|
||||||
.select2-container--open {
|
.select2-container--open {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
@@ -85,10 +145,11 @@
|
|||||||
.select2-results {
|
.select2-results {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-results__option {
|
.select2-results__option {
|
||||||
padding: 8px 8px;
|
padding: 6px 8px;
|
||||||
color: #212529;
|
color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,48 +200,54 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-9">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<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>
|
||||||
{% if preselected_customer %}
|
<div class="customer-select-wrapper">
|
||||||
<select name="customer"
|
{% if preselected_customer %}
|
||||||
class="form-select customer-select2-auto"
|
<select name="customer"
|
||||||
id="id_customer"
|
class="form-select customer-select2-auto"
|
||||||
data-ajax-url="{% url 'customers:api-search-customers' %}"
|
id="id_customer"
|
||||||
data-create-url="{% url 'customers:api-create-customer' %}"
|
data-ajax-url="{% url 'customers:api-search-customers' %}"
|
||||||
data-modal-id="createCustomerModal">
|
data-create-url="{% url 'customers:api-create-customer' %}"
|
||||||
<option value="{{ preselected_customer.pk }}" selected
|
data-modal-id="createCustomerModal">
|
||||||
data-name="{{ preselected_customer.name }}"
|
<option value="{{ preselected_customer.pk }}" selected
|
||||||
data-phone="{{ preselected_customer.phone|default:'' }}"
|
data-name="{{ preselected_customer.name }}"
|
||||||
data-email="{{ preselected_customer.email|default:'' }}">
|
data-phone="{{ preselected_customer.phone|default:'' }}"
|
||||||
{{ preselected_customer.name }}{% if preselected_customer.phone %} ({{ preselected_customer.phone }}){% endif %}
|
data-email="{{ preselected_customer.email|default:'' }}">
|
||||||
</option>
|
{{ preselected_customer.name }}{% if preselected_customer.phone %} ({{ preselected_customer.phone }}){% endif %}
|
||||||
</select>
|
|
||||||
{% else %}
|
|
||||||
<select name="customer"
|
|
||||||
class="form-select customer-select2-auto"
|
|
||||||
id="id_customer"
|
|
||||||
data-ajax-url="{% url 'customers:api-search-customers' %}"
|
|
||||||
data-create-url="{% url 'customers:api-create-customer' %}"
|
|
||||||
data-modal-id="createCustomerModal">
|
|
||||||
{% if form.customer.value %}
|
|
||||||
<option value="{{ form.customer.value }}" selected
|
|
||||||
data-name="{{ form.instance.customer.name }}"
|
|
||||||
data-phone="{{ form.instance.customer.phone|default:'' }}"
|
|
||||||
data-email="{{ form.instance.customer.email|default:'' }}">
|
|
||||||
{{ form.instance.customer.name }}{% if form.instance.customer.phone %} ({{ form.instance.customer.phone }}){% endif %}
|
|
||||||
</option>
|
</option>
|
||||||
{% endif %}
|
</select>
|
||||||
</select>
|
{% else %}
|
||||||
{% endif %}
|
<select name="customer"
|
||||||
|
class="form-select customer-select2-auto"
|
||||||
|
id="id_customer"
|
||||||
|
data-ajax-url="{% url 'customers:api-search-customers' %}"
|
||||||
|
data-create-url="{% url 'customers:api-create-customer' %}"
|
||||||
|
data-modal-id="createCustomerModal">
|
||||||
|
{% if form.customer.value %}
|
||||||
|
<option value="{{ form.customer.value }}" selected
|
||||||
|
data-name="{{ form.instance.customer.name }}"
|
||||||
|
data-phone="{{ form.instance.customer.phone|default:'' }}"
|
||||||
|
data-email="{{ form.instance.customer.email|default:'' }}">
|
||||||
|
{{ form.instance.customer.name }}{% if form.instance.customer.phone %} ({{ form.instance.customer.phone }}){% endif %}
|
||||||
|
</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-outline-warning" id="select-system-customer-btn"
|
||||||
|
title="Выбрать анонимного покупателя" data-system-url="{% url 'customers:api-get-system-customer' %}">
|
||||||
|
<i class="bi bi-incognito"></i> Аноним
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{% 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 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.status.id_for_label }}" class="form-label">
|
<label for="{{ form.status.id_for_label }}" class="form-label">
|
||||||
Статус <span class="text-danger">*</span>
|
Статус <span class="text-danger">*</span>
|
||||||
@@ -1015,6 +1082,79 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<!-- Customer Select2 Widget -->
|
<!-- Customer Select2 Widget -->
|
||||||
<script src="{% static 'orders/js/customer_select2.js' %}" defer></script>
|
<script src="{% static 'orders/js/customer_select2.js' %}" defer></script>
|
||||||
|
|
||||||
|
<!-- System Customer Button Script -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function initSystemCustomerButton() {
|
||||||
|
const button = document.getElementById('select-system-customer-btn');
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const systemUrl = button.dataset.systemUrl;
|
||||||
|
const customerSelect = document.getElementById('id_customer');
|
||||||
|
if (!customerSelect) return;
|
||||||
|
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
// Показываем индикатор загрузки
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Загрузка...';
|
||||||
|
|
||||||
|
fetch(systemUrl)
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success && data.customer) {
|
||||||
|
// Обновляем Select2
|
||||||
|
if (typeof $ !== 'undefined') {
|
||||||
|
const $select = $(customerSelect);
|
||||||
|
// Удаляем текущую опцию, если есть
|
||||||
|
$select.empty();
|
||||||
|
// Добавляем нового клиента
|
||||||
|
const newOption = new Option(
|
||||||
|
data.customer.name,
|
||||||
|
data.customer.id,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
// Добавляем data-атрибуты
|
||||||
|
$(newOption).attr({
|
||||||
|
'data-name': data.customer.name,
|
||||||
|
'data-phone': data.customer.phone,
|
||||||
|
'data-email': data.customer.email,
|
||||||
|
'data-is-system-customer': 'true'
|
||||||
|
});
|
||||||
|
$select.append(newOption).trigger('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем уведомление
|
||||||
|
if (window.showNotification) {
|
||||||
|
window.showNotification('Выбран анонимный покупатель', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Error loading system customer:', error);
|
||||||
|
if (window.showNotification) {
|
||||||
|
window.showNotification('Ошибка при загрузке системного клиента', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при загрузке DOM
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initSystemCustomerButton);
|
||||||
|
} else {
|
||||||
|
initSystemCustomerButton();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Инициализация Select2 для остальных полей (после jQuery загружен)
|
// Инициализация Select2 для остальных полей (после jQuery загружен)
|
||||||
if (typeof $ !== 'undefined') {
|
if (typeof $ !== 'undefined') {
|
||||||
|
|||||||
Reference in New Issue
Block a user