Добавлена возможность выбора анонимного системного клиента в форме заказа

- Убрана фильтрация системного клиента из результатов поиска (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:
2026-01-15 12:54:56 +03:00
parent c7e03d258b
commit 2ef537fff6
4 changed files with 218 additions and 35 deletions

View File

@@ -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'),
] ]

View File

@@ -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,
}) })
# Если ничего не найдено, предлагаем создать нового клиента # Если ничего не найдено, предлагаем создать нового клиента

View File

@@ -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;
} }

View File

@@ -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') {