Files
octopus/myproject/customers/views.py
Andrey Smakotin 74ece6dd66 Добавлен универсальный поиск клиента и быстрое создание нового клиента
Реализованы следующие функции:
- AJAX API endpoint для поиска клиента по имени, телефону или email одновременно
- AJAX API endpoint для создания нового клиента прямо при создании заказа
- Интерактивная форма поиска в поле "Клиент" с использованием Select2
- При отсутствии результатов поиска предлагается создать нового клиента с автоматическим заполнением формы введенными данными
- Модальное окно для создания клиента во всплывающем окне (не на отдельной странице)
- Автоматический выбор созданного клиента после сохранения

Изменения:
1. customers/views.py - добавлены endpoints api_search_customers и api_create_customer
2. customers/urls.py - добавлены URL маршруты для новых endpoints
3. orders/templates/orders/order_form.html - обновлена инициализация Select2 для поиска, добавлено модальное окно и стили

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 22:23:11 +03:00

281 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.core.paginator import Paginator
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
import phonenumbers
import json
from .models import Customer, Address
from .forms import CustomerForm
def normalize_query_phone(q):
"""Normalize phone number for search"""
try:
parsed = phonenumbers.parse(q, "BY")
if phonenumbers.is_valid_number(parsed):
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
return q
except:
return q
def customer_list(request):
"""Список всех клиентов"""
query = request.GET.get('q')
customers = Customer.objects.all()
if query:
# Try to normalize the phone number for searching
phone_normalized = normalize_query_phone(query)
customers = customers.filter(
Q(name__icontains=query) |
Q(email__icontains=query) |
Q(phone__icontains=phone_normalized)
)
customers = customers.order_by('-created_at')
# Пагинация
paginator = Paginator(customers, 25) # 25 клиентов на страницу
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'query': query,
}
return render(request, 'customers/customer_list.html', context)
def customer_detail(request, pk):
"""Детали клиента"""
customer = get_object_or_404(Customer, pk=pk)
addresses = customer.addresses.all()
context = {
'customer': customer,
'addresses': addresses,
}
return render(request, 'customers/customer_detail.html', context)
def customer_create(request):
"""Создание нового клиента"""
if request.method == 'POST':
form = CustomerForm(request.POST)
if form.is_valid():
customer = form.save()
messages.success(request, f'Клиент {customer.full_name} успешно создан.')
return redirect('customers:customer-detail', pk=customer.pk)
else:
form = CustomerForm()
return render(request, 'customers/customer_form.html', {'form': form, 'is_creating': True})
def customer_update(request, pk):
"""Редактирование клиента"""
customer = get_object_or_404(Customer, pk=pk)
if request.method == 'POST':
form = CustomerForm(request.POST, instance=customer)
if form.is_valid():
form.save()
messages.success(request, f'Клиент {customer.full_name} успешно обновлён.')
return redirect('customers:customer-detail', pk=customer.pk)
else:
form = CustomerForm(instance=customer)
return render(request, 'customers/customer_form.html', {'form': form, 'is_creating': False})
def customer_delete(request, pk):
"""Удаление клиента"""
customer = get_object_or_404(Customer, pk=pk)
if request.method == 'POST':
customer_name = customer.full_name
customer.delete()
messages.success(request, f'Клиент {customer_name} успешно удален.')
return redirect('customers:customer-list')
context = {
'customer': customer
}
return render(request, 'customers/customer_confirm_delete.html', context)
# === AJAX API ENDPOINTS ===
@require_http_methods(["GET"])
@login_required
def api_search_customers(request):
"""
AJAX endpoint для поиска клиента по имени, телефону или email.
Параметры GET:
- q: поисковая строка
Возвращает JSON с результатами поиска:
{
"results": [
{"id": 1, "text": "Иван Петров (+375291234567)", "name": "Иван Петров", "phone": "+375291234567", "email": "ivan@example.com"},
...
],
"pagination": {"more": false}
}
Если ничего не найдено и заданы параметры поиска, возвращает:
{
"results": [
{"id": null, "text": "Создать клиента: 'Поиск'", "is_create_option": true, "search_text": "Поиск"}
],
"pagination": {"more": false}
}
"""
query = request.GET.get('q', '').strip()
if not query or len(query) < 1:
return JsonResponse({
'results': [],
'pagination': {'more': False}
})
# Пытаемся нормализовать номер телефона для поиска
phone_normalized = normalize_query_phone(query)
# Ищем по имени, email или телефону
customers = Customer.objects.filter(
Q(name__icontains=query) |
Q(email__icontains=query) |
Q(phone__icontains=phone_normalized)
).order_by('name')[:20] # Ограничиваем 20 результатами
results = []
# Добавляем найденные клиентов
for customer in customers:
phone_display = str(customer.phone) if customer.phone else ''
text = customer.name
if phone_display:
text += f' ({phone_display})'
results.append({
'id': customer.pk,
'text': text,
'name': customer.name,
'phone': phone_display,
'email': customer.email,
})
# Если ничего не найдено, предлагаем создать нового клиента
if not results:
results.append({
'id': None,
'text': f'Создать клиента: "{query}"',
'is_create_option': True,
'search_text': query,
})
return JsonResponse({
'results': results,
'pagination': {'more': False}
})
@require_http_methods(["POST"])
@login_required
def api_create_customer(request):
"""
AJAX endpoint для создания нового клиента.
Принимает POST JSON:
{
"name": "Иван Петров",
"phone": "+375291234567",
"email": "ivan@example.com"
}
Возвращает JSON:
{
"success": true,
"id": 123,
"name": "Иван Петров",
"phone": "+375291234567",
"email": "ivan@example.com"
}
При ошибке:
{
"success": false,
"error": "Клиент с таким номером телефона уже существует"
}
"""
try:
data = json.loads(request.body)
name = data.get('name', '').strip()
phone = data.get('phone', '').strip()
email = data.get('email', '').strip()
# Валидация: имя обязательно
if not name:
return JsonResponse({
'success': False,
'error': 'Имя клиента обязательно'
}, status=400)
# Нормализуем телефон если он указан
if phone:
phone = normalize_query_phone(phone)
# Проверяем, не существует ли уже клиент с таким телефоном
if phone and Customer.objects.filter(phone=phone).exists():
return JsonResponse({
'success': False,
'error': 'Клиент с таким номером телефона уже существует'
}, status=400)
# Проверяем, не существует ли уже клиент с таким email
if email and Customer.objects.filter(email=email).exists():
return JsonResponse({
'success': False,
'error': 'Клиент с таким email уже существует'
}, status=400)
# Создаем нового клиента
customer = Customer.objects.create(
name=name,
phone=phone if phone else None,
email=email if email else None
)
phone_display = str(customer.phone) if customer.phone else ''
return JsonResponse({
'success': True,
'id': customer.pk,
'name': customer.name,
'phone': phone_display,
'email': customer.email,
}, status=201)
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Некорректный JSON'
}, status=400)
except ValidationError as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=400)
except Exception as e:
return JsonResponse({
'success': False,
'error': f'Ошибка сервера: {str(e)}'
}, status=500)