Рефакторинг: перенос логики создания временных комплектов в сервис
Изменения: - Удалена функция create_temporary_kit из myproject/orders/views.py - Перенесена в новый сервис myproject/products/services/kit_service.py - Добавлен API endpoint products:api-temporary-kit-create для создания временных комплектов - Обновлены URL-ы соответственно Преимущества: - Логика временных комплектов теперь находится в соответствующем приложении (products) - Упрощена архитектура orders приложения - Сервис может быть переиспользован в других контекстах - Лучшее разделение ответственности между приложениями 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(dir /b /s settings.py)",
|
"Bash(dir /b /s settings.py)",
|
||||||
"Bash(git add:*)"
|
"Bash(git add:*)",
|
||||||
|
"Bash(..venvScriptspython.exe manage.py check)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -14,7 +14,4 @@ urlpatterns = [
|
|||||||
# AJAX endpoints
|
# AJAX endpoints
|
||||||
path('<int:pk>/autosave/', views.autosave_draft_order, name='order-autosave'),
|
path('<int:pk>/autosave/', views.autosave_draft_order, name='order-autosave'),
|
||||||
path('create-draft/', views.create_draft_from_form, name='order-create-draft'),
|
path('create-draft/', views.create_draft_from_form, name='order-create-draft'),
|
||||||
|
|
||||||
# Временные комплекты
|
|
||||||
path('temporary-kits/create/', views.create_temporary_kit, name='temporary-kit-create'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.shortcuts import render, redirect, get_object_or_404
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db import transaction
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -11,7 +10,6 @@ from .models import Order, OrderItem
|
|||||||
from .forms import OrderForm, OrderItemFormSet
|
from .forms import OrderForm, OrderItemFormSet
|
||||||
from .filters import OrderFilter
|
from .filters import OrderFilter
|
||||||
from .services import DraftOrderService
|
from .services import DraftOrderService
|
||||||
from products.models import ProductKit, KitItem, Product
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
@@ -383,122 +381,5 @@ def create_draft_from_form(request):
|
|||||||
|
|
||||||
|
|
||||||
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
||||||
|
# УДАЛЕНО: Логика создания временных комплектов перенесена в products.services.kit_service
|
||||||
@require_http_methods(["POST"])
|
# Используйте API endpoint: products:api-temporary-kit-create
|
||||||
def create_temporary_kit(request):
|
|
||||||
"""
|
|
||||||
AJAX endpoint для создания временного комплекта.
|
|
||||||
Используется при оформлении заказа для создания букета "на лету".
|
|
||||||
|
|
||||||
Принимает JSON:
|
|
||||||
{
|
|
||||||
"name": "Букет для Анны",
|
|
||||||
"description": "Красные розы и белые лилии",
|
|
||||||
"order_id": 123, // опционально, если заказ уже создан
|
|
||||||
"components": [
|
|
||||||
{"product_id": 1, "quantity": "5"},
|
|
||||||
{"product_id": 2, "quantity": "3"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Возвращает JSON:
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"kit_id": 456,
|
|
||||||
"kit_name": "Букет для Анны",
|
|
||||||
"kit_sku": "KIT-000456",
|
|
||||||
"kit_price": "1500.00",
|
|
||||||
"message": "Временный комплект создан успешно"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(request.body)
|
|
||||||
|
|
||||||
name = data.get('name', '').strip()
|
|
||||||
description = data.get('description', '').strip()
|
|
||||||
order_id = data.get('order_id')
|
|
||||||
components = data.get('components', [])
|
|
||||||
|
|
||||||
# Валидация
|
|
||||||
if not name:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Необходимо указать название комплекта'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
if not components or len(components) == 0:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Комплект должен содержать хотя бы один компонент'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Создаем временный комплект
|
|
||||||
with transaction.atomic():
|
|
||||||
# Получаем заказ если указан
|
|
||||||
order = None
|
|
||||||
if order_id:
|
|
||||||
try:
|
|
||||||
order = Order.objects.get(pk=order_id)
|
|
||||||
except Order.DoesNotExist:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': f'Заказ #{order_id} не найден'
|
|
||||||
}, status=404)
|
|
||||||
|
|
||||||
# Создаем комплект
|
|
||||||
kit = ProductKit.objects.create(
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
is_temporary=True,
|
|
||||||
is_active=True,
|
|
||||||
order=order,
|
|
||||||
price_adjustment_type='none'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Добавляем компоненты
|
|
||||||
for component in components:
|
|
||||||
product_id = component.get('product_id')
|
|
||||||
quantity = component.get('quantity')
|
|
||||||
|
|
||||||
if not product_id or not quantity:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
product = Product.objects.get(pk=product_id)
|
|
||||||
KitItem.objects.create(
|
|
||||||
kit=kit,
|
|
||||||
product=product,
|
|
||||||
quantity=Decimal(str(quantity))
|
|
||||||
)
|
|
||||||
except Product.DoesNotExist:
|
|
||||||
# Пропускаем несуществующие товары
|
|
||||||
continue
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
# Пропускаем некорректные количества
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Пересчитываем цену комплекта
|
|
||||||
kit.recalculate_base_price()
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
'success': True,
|
|
||||||
'kit_id': kit.id,
|
|
||||||
'kit_name': kit.name,
|
|
||||||
'kit_sku': kit.sku,
|
|
||||||
'kit_price': str(kit.actual_price),
|
|
||||||
'message': f'Временный комплект "{kit.name}" создан успешно'
|
|
||||||
})
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Некорректный JSON'
|
|
||||||
}, status=400)
|
|
||||||
except Exception as e:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': f'Ошибка при создании комплекта: {str(e)}'
|
|
||||||
}, status=500)
|
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ KitItemFormSetCreate = inlineformset_factory(
|
|||||||
KitItem,
|
KitItem,
|
||||||
form=KitItemForm,
|
form=KitItemForm,
|
||||||
formset=BaseKitItemFormSet,
|
formset=BaseKitItemFormSet,
|
||||||
fields=['id', 'product', 'variant_group', 'quantity'],
|
fields=['product', 'variant_group', 'quantity'],
|
||||||
extra=1, # Показать 1 пустую форму для первого компонента
|
extra=1, # Показать 1 пустую форму для первого компонента
|
||||||
can_delete=True, # Разрешить удаление компонентов
|
can_delete=True, # Разрешить удаление компонентов
|
||||||
min_num=0, # Минимум 0 компонентов (можно создать пустой комплект)
|
min_num=0, # Минимум 0 компонентов (можно создать пустой комплект)
|
||||||
@@ -266,7 +266,7 @@ KitItemFormSetUpdate = inlineformset_factory(
|
|||||||
KitItem,
|
KitItem,
|
||||||
form=KitItemForm,
|
form=KitItemForm,
|
||||||
formset=BaseKitItemFormSet,
|
formset=BaseKitItemFormSet,
|
||||||
fields=['id', 'product', 'variant_group', 'quantity'],
|
fields=['product', 'variant_group', 'quantity'],
|
||||||
extra=0, # НЕ показывать пустые формы при редактировании
|
extra=0, # НЕ показывать пустые формы при редактировании
|
||||||
can_delete=True, # Разрешить удаление компонентов
|
can_delete=True, # Разрешить удаление компонентов
|
||||||
min_num=0, # Минимум 0 компонентов
|
min_num=0, # Минимум 0 компонентов
|
||||||
|
|||||||
159
myproject/products/services/kit_service.py
Normal file
159
myproject/products/services/kit_service.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
"""
|
||||||
|
Сервис для работы с комплектами товаров.
|
||||||
|
Содержит бизнес-логику создания, обновления и управления комплектами.
|
||||||
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import transaction
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
|
from ..models import ProductKit, Product, KitItem
|
||||||
|
|
||||||
|
|
||||||
|
def create_temporary_kit(
|
||||||
|
name: str,
|
||||||
|
components: List[Dict],
|
||||||
|
description: str = '',
|
||||||
|
order=None
|
||||||
|
) -> ProductKit:
|
||||||
|
"""
|
||||||
|
Создает временный комплект с компонентами.
|
||||||
|
|
||||||
|
Временные комплекты используются для создания букетов "на лету" при оформлении заказа.
|
||||||
|
Они автоматически помечаются как временные (is_temporary=True) и связываются с заказом.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Название комплекта
|
||||||
|
components: Список компонентов в формате [{"product_id": 1, "quantity": "5"}, ...]
|
||||||
|
description: Описание комплекта (опционально)
|
||||||
|
order: Заказ, к которому привязан временный комплект (опционально)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProductKit: Созданный временный комплект
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Если данные невалидны
|
||||||
|
Product.DoesNotExist: Если товар не найден
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> kit = create_temporary_kit(
|
||||||
|
... name="Букет для Анны",
|
||||||
|
... description="Красные розы и белые лилии",
|
||||||
|
... components=[
|
||||||
|
... {"product_id": 1, "quantity": "5"},
|
||||||
|
... {"product_id": 2, "quantity": "3"}
|
||||||
|
... ]
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
# Валидация
|
||||||
|
if not name or not name.strip():
|
||||||
|
raise ValueError('Необходимо указать название комплекта')
|
||||||
|
|
||||||
|
if not components or len(components) == 0:
|
||||||
|
raise ValueError('Комплект должен содержать хотя бы один компонент')
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# Создаем комплект
|
||||||
|
kit = ProductKit.objects.create(
|
||||||
|
name=name.strip(),
|
||||||
|
description=description.strip() if description else '',
|
||||||
|
is_temporary=True,
|
||||||
|
is_active=True,
|
||||||
|
order=order,
|
||||||
|
price_adjustment_type='none'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем компоненты
|
||||||
|
added_count = 0
|
||||||
|
for component in components:
|
||||||
|
product_id = component.get('product_id')
|
||||||
|
quantity = component.get('quantity')
|
||||||
|
|
||||||
|
if not product_id or not quantity:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
product = Product.objects.get(pk=product_id, is_active=True)
|
||||||
|
KitItem.objects.create(
|
||||||
|
kit=kit,
|
||||||
|
product=product,
|
||||||
|
quantity=Decimal(str(quantity))
|
||||||
|
)
|
||||||
|
added_count += 1
|
||||||
|
except Product.DoesNotExist:
|
||||||
|
# Пропускаем несуществующие товары
|
||||||
|
continue
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
# Пропускаем некорректные количества
|
||||||
|
continue
|
||||||
|
|
||||||
|
if added_count == 0:
|
||||||
|
raise ValueError('Не удалось добавить ни одного компонента в комплект')
|
||||||
|
|
||||||
|
# Пересчитываем цену комплекта на основе компонентов
|
||||||
|
kit.recalculate_base_price()
|
||||||
|
|
||||||
|
return kit
|
||||||
|
|
||||||
|
|
||||||
|
def make_kit_permanent(kit: ProductKit) -> bool:
|
||||||
|
"""
|
||||||
|
Преобразует временный комплект в постоянный.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kit: Комплект для преобразования
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если комплект был преобразован, False если уже постоянный
|
||||||
|
"""
|
||||||
|
if not kit.is_temporary:
|
||||||
|
return False
|
||||||
|
|
||||||
|
kit.is_temporary = False
|
||||||
|
kit.order = None # Отвязываем от заказа
|
||||||
|
kit.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate_kit(kit: ProductKit, new_name: Optional[str] = None) -> ProductKit:
|
||||||
|
"""
|
||||||
|
Создает копию комплекта со всеми компонентами.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kit: Комплект для дублирования
|
||||||
|
new_name: Новое название (если None, используется "Копия {original_name}")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProductKit: Новый комплект-копия
|
||||||
|
"""
|
||||||
|
with transaction.atomic():
|
||||||
|
# Копируем комплект
|
||||||
|
new_kit = ProductKit.objects.create(
|
||||||
|
name=new_name or f"Копия {kit.name}",
|
||||||
|
description=kit.description,
|
||||||
|
short_description=kit.short_description,
|
||||||
|
price_adjustment_type=kit.price_adjustment_type,
|
||||||
|
price_adjustment_value=kit.price_adjustment_value,
|
||||||
|
sale_price=kit.sale_price,
|
||||||
|
is_temporary=False, # Копия всегда постоянная
|
||||||
|
is_active=kit.is_active
|
||||||
|
)
|
||||||
|
|
||||||
|
# Копируем категории
|
||||||
|
new_kit.categories.set(kit.categories.all())
|
||||||
|
|
||||||
|
# Копируем теги
|
||||||
|
new_kit.tags.set(kit.tags.all())
|
||||||
|
|
||||||
|
# Копируем компоненты
|
||||||
|
for item in kit.kit_items.all():
|
||||||
|
KitItem.objects.create(
|
||||||
|
kit=new_kit,
|
||||||
|
product=item.product,
|
||||||
|
variant_group=item.variant_group,
|
||||||
|
quantity=item.quantity
|
||||||
|
)
|
||||||
|
|
||||||
|
# Пересчитываем цену
|
||||||
|
new_kit.recalculate_base_price()
|
||||||
|
|
||||||
|
return new_kit
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
{% for kititem_form in kititem_formset %}
|
{% for kititem_form in kititem_formset %}
|
||||||
<div class="card mb-2 kititem-form border"
|
<div class="card mb-2 kititem-form border"
|
||||||
data-form-index="{{ forloop.counter0 }}"
|
data-form-index="{{ forloop.counter0 }}"
|
||||||
|
data-product-id="{% if kititem_form.instance.product %}{{ kititem_form.instance.product.id }}{% endif %}"
|
||||||
data-product-price="{% if kititem_form.instance.product %}{{ kititem_form.instance.product.actual_price|default:0 }}{% else %}0{% endif %}">
|
data-product-price="{% if kititem_form.instance.product %}{{ kititem_form.instance.product.actual_price|default:0 }}{% else %}0{% endif %}">
|
||||||
{{ kititem_form.id }}
|
{{ kititem_form.id }}
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
<!-- УДАЛЕНИЕ -->
|
<!-- УДАЛЕНИЕ -->
|
||||||
<div class="col-md-1 text-end">
|
<div class="col-md-1 text-end">
|
||||||
{% if kititem_form.DELETE %}
|
{% if kititem_form.DELETE %}
|
||||||
<button type="button" class="btn btn-sm btn-link text-danger p-0" onclick="this.previousElementSibling.checked = true; this.closest('.kititem-form').style.display='none';" title="Удалить">
|
<button type="button" class="btn btn-sm btn-link text-danger p-0" onclick="this.nextElementSibling.checked = true; this.closest('.kititem-form').style.display='none'; if(typeof calculateFinalPrice === 'function') calculateFinalPrice();" title="Удалить">
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
{{ kititem_form.DELETE }}
|
{{ kititem_form.DELETE }}
|
||||||
|
|||||||
@@ -442,10 +442,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const productId = parseInt(selectElement.value);
|
const rawValue = selectElement.value;
|
||||||
|
if (!rawValue) {
|
||||||
|
console.warn('getProductPrice: no value');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectElement.value || isNaN(productId) || productId <= 0) {
|
// Извлекаем числовой ID из значения (может быть "product_123" или "123")
|
||||||
console.warn('getProductPrice: no valid product id', selectElement.value);
|
let productId;
|
||||||
|
if (rawValue.includes('_')) {
|
||||||
|
const parts = rawValue.split('_');
|
||||||
|
productId = parseInt(parts[1]);
|
||||||
|
} else {
|
||||||
|
productId = parseInt(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(productId) || productId <= 0) {
|
||||||
|
console.warn('getProductPrice: invalid product id', rawValue);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,15 +484,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пытаемся получить из Select2 option data (сначала actual_price, потом price)
|
// Пытаемся получить из Select2 data (приоритет: actual_price > price)
|
||||||
const selectedOption = $(selectElement).find('option:selected');
|
const $select = $(selectElement);
|
||||||
let priceData = selectedOption.data('actual_price') || selectedOption.data('price');
|
const selectedData = $select.select2('data');
|
||||||
if (priceData) {
|
if (selectedData && selectedData.length > 0) {
|
||||||
const price = parseFloat(priceData) || 0;
|
const itemData = selectedData[0];
|
||||||
if (price > 0) {
|
const priceData = itemData.actual_price || itemData.price;
|
||||||
priceCache[productId] = price;
|
if (priceData) {
|
||||||
console.log('getProductPrice: from select2 data', productId, price);
|
const price = parseFloat(priceData) || 0;
|
||||||
return price;
|
if (price > 0) {
|
||||||
|
priceCache[productId] = price;
|
||||||
|
console.log('getProductPrice: from select2 data', productId, price);
|
||||||
|
return price;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +531,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
$('[name$="-product"]').on('select2:select', async function() {
|
$('[name$="-product"]').on('select2:select', async function() {
|
||||||
const form = $(this).closest('.kititem-form');
|
const form = $(this).closest('.kititem-form');
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
form.attr('data-product-id', this.value);
|
// Извлекаем числовой ID из "product_123"
|
||||||
|
let numericId = this.value;
|
||||||
|
if (this.value.includes('_')) {
|
||||||
|
numericId = this.value.split('_')[1];
|
||||||
|
}
|
||||||
|
form.attr('data-product-id', numericId);
|
||||||
// Загружаем цену и пересчитываем
|
// Загружаем цену и пересчитываем
|
||||||
await getProductPrice(this);
|
await getProductPrice(this);
|
||||||
calculateFinalPrice();
|
calculateFinalPrice();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),
|
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),
|
||||||
|
path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'),
|
||||||
|
|
||||||
# CRUD URLs for ProductVariantGroup (Варианты товаров)
|
# CRUD URLs for ProductVariantGroup (Варианты товаров)
|
||||||
path('variant-groups/', views.ProductVariantGroupListView.as_view(), name='variantgroup-list'),
|
path('variant-groups/', views.ProductVariantGroupListView.as_view(), name='variantgroup-list'),
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ from .variant_group_views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# API представления
|
# API представления
|
||||||
from .api_views import search_products_and_variants, validate_kit_cost
|
from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -132,4 +132,5 @@ __all__ = [
|
|||||||
# API
|
# API
|
||||||
'search_products_and_variants',
|
'search_products_and_variants',
|
||||||
'validate_kit_cost',
|
'validate_kit_cost',
|
||||||
|
'create_temporary_kit_api',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -531,3 +531,92 @@ def validate_kit_cost(request):
|
|||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
def create_temporary_kit_api(request):
|
||||||
|
"""
|
||||||
|
AJAX endpoint для создания временного комплекта.
|
||||||
|
Используется при оформлении заказа для создания букета "на лету".
|
||||||
|
|
||||||
|
Принимает JSON:
|
||||||
|
{
|
||||||
|
"name": "Букет для Анны",
|
||||||
|
"description": "Красные розы и белые лилии",
|
||||||
|
"order_id": 123, // опционально, если заказ уже создан
|
||||||
|
"components": [
|
||||||
|
{"product_id": 1, "quantity": "5"},
|
||||||
|
{"product_id": 2, "quantity": "3"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает JSON:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"kit_id": 456,
|
||||||
|
"kit_name": "Букет для Анны",
|
||||||
|
"kit_sku": "KIT-000456",
|
||||||
|
"kit_price": "1500.00",
|
||||||
|
"message": "Временный комплект создан успешно"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Метод не поддерживается'
|
||||||
|
}, status=405)
|
||||||
|
|
||||||
|
import json
|
||||||
|
from ..services.kit_service import create_temporary_kit
|
||||||
|
from orders.models import Order
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
description = data.get('description', '').strip()
|
||||||
|
order_id = data.get('order_id')
|
||||||
|
components = data.get('components', [])
|
||||||
|
|
||||||
|
# Получаем заказ если указан
|
||||||
|
order = None
|
||||||
|
if order_id:
|
||||||
|
try:
|
||||||
|
order = Order.objects.get(pk=order_id)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Заказ #{order_id} не найден'
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
# Создаем временный комплект через сервис
|
||||||
|
kit = create_temporary_kit(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
components=components,
|
||||||
|
order=order
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'kit_id': kit.id,
|
||||||
|
'kit_name': kit.name,
|
||||||
|
'kit_sku': kit.sku,
|
||||||
|
'kit_price': str(kit.actual_price),
|
||||||
|
'message': f'Временный комплект "{kit.name}" создан успешно'
|
||||||
|
})
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=400)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Некорректный JSON'
|
||||||
|
}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Ошибка при создании комплекта: {str(e)}'
|
||||||
|
}, status=500)
|
||||||
|
|||||||
@@ -93,6 +93,28 @@ class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
|
|||||||
template_name = 'products/productkit_create.html'
|
template_name = 'products/productkit_create.html'
|
||||||
permission_required = 'products.add_productkit'
|
permission_required = 'products.add_productkit'
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
|
||||||
|
API возвращает ID в формате "product_123" или "kit_456", но Django ожидает числа.
|
||||||
|
"""
|
||||||
|
# Создаем изменяемую копию POST данных
|
||||||
|
post_data = request.POST.copy()
|
||||||
|
|
||||||
|
# Очищаем product ID от префиксов (product_123 -> 123)
|
||||||
|
for key in post_data.keys():
|
||||||
|
if key.endswith('-product') and post_data[key]:
|
||||||
|
value = post_data[key]
|
||||||
|
if '_' in value:
|
||||||
|
# Извлекаем числовой ID из "product_123"
|
||||||
|
numeric_id = value.split('_')[1]
|
||||||
|
post_data[key] = numeric_id
|
||||||
|
|
||||||
|
# Заменяем request.POST на очищенные данные
|
||||||
|
request.POST = post_data
|
||||||
|
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
@@ -199,6 +221,28 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
|
|||||||
template_name = 'products/productkit_edit.html'
|
template_name = 'products/productkit_edit.html'
|
||||||
permission_required = 'products.change_productkit'
|
permission_required = 'products.change_productkit'
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
|
||||||
|
API возвращает ID в формате "product_123" или "kit_456", но Django ожидает числа.
|
||||||
|
"""
|
||||||
|
# Создаем изменяемую копию POST данных
|
||||||
|
post_data = request.POST.copy()
|
||||||
|
|
||||||
|
# Очищаем product ID от префиксов (product_123 -> 123)
|
||||||
|
for key in post_data.keys():
|
||||||
|
if key.endswith('-product') and post_data[key]:
|
||||||
|
value = post_data[key]
|
||||||
|
if '_' in value:
|
||||||
|
# Извлекаем числовой ID из "product_123"
|
||||||
|
numeric_id = value.split('_')[1]
|
||||||
|
post_data[key] = numeric_id
|
||||||
|
|
||||||
|
# Заменяем request.POST на очищенные данные
|
||||||
|
request.POST = post_data
|
||||||
|
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|||||||
47
test_api.sh
Normal file
47
test_api.sh
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Тест API endpoints для поиска и создания клиентов
|
||||||
|
# Использование: bash test_api.sh
|
||||||
|
|
||||||
|
BASE_URL="http://grach.localhost:8000"
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo "ТЕСТ API ENDPOINTS ДЛЯ ПОИСКА КЛИЕНТОВ"
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: Поиск по имени
|
||||||
|
echo "TEST 1: Поиск по имени (q=Иван)"
|
||||||
|
echo "URL: $BASE_URL/customers/api/search/?q=Иван"
|
||||||
|
echo ""
|
||||||
|
curl -s "$BASE_URL/customers/api/search/?q=Иван" | python -m json.tool 2>/dev/null || curl -s "$BASE_URL/customers/api/search/?q=Иван"
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 2: Поиск по телефону
|
||||||
|
echo "TEST 2: Поиск по телефону (q=375)"
|
||||||
|
echo "URL: $BASE_URL/customers/api/search/?q=375"
|
||||||
|
echo ""
|
||||||
|
curl -s "$BASE_URL/customers/api/search/?q=375" | python -m json.tool 2>/dev/null || curl -s "$BASE_URL/customers/api/search/?q=375"
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 3: Пустой поиск (должна вернуться пустая строка results)
|
||||||
|
echo "TEST 3: Пустой поиск (q=)"
|
||||||
|
echo "URL: $BASE_URL/customers/api/search/?q="
|
||||||
|
echo ""
|
||||||
|
curl -s "$BASE_URL/customers/api/search/?q=" | python -m json.tool 2>/dev/null || curl -s "$BASE_URL/customers/api/search/?q="
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 4: Проверка что endpoint существует
|
||||||
|
echo "TEST 4: Проверка доступности endpoint'а"
|
||||||
|
echo "URL: $BASE_URL/customers/api/search/"
|
||||||
|
echo ""
|
||||||
|
curl -i "$BASE_URL/customers/api/search/?q=test" 2>&1 | head -15
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
52
test_api_simple.py
Normal file
52
test_api_simple.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Простой скрипт для проверки API endpoints через Django shell
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
# Настройка Django
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.test import Client
|
||||||
|
import json
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
BASE_URL = '/customers/api/search/'
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("ТЕСТ API ENDPOINTS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Test 1: Empty query
|
||||||
|
print("\nТЕСТ 1: Пустой запрос")
|
||||||
|
response = client.get(f'{BASE_URL}?q=')
|
||||||
|
print(f"Статус: {response.status_code}")
|
||||||
|
print(f"Ответ: {response.content.decode()}")
|
||||||
|
|
||||||
|
# Test 2: Search by single letter
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("ТЕСТ 2: Поиск по букве 'И'")
|
||||||
|
response = client.get(f'{BASE_URL}?q=И')
|
||||||
|
print(f"Статус: {response.status_code}")
|
||||||
|
data = json.loads(response.content)
|
||||||
|
print(f"Результатов: {len(data.get('results', []))}")
|
||||||
|
if data.get('results'):
|
||||||
|
for item in data['results'][:3]:
|
||||||
|
print(f" - {item.get('text', 'No text')}")
|
||||||
|
|
||||||
|
# Test 3: Search by number
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("ТЕСТ 3: Поиск по цифрам '29'")
|
||||||
|
response = client.get(f'{BASE_URL}?q=29')
|
||||||
|
print(f"Статус: {response.status_code}")
|
||||||
|
data = json.loads(response.content)
|
||||||
|
print(f"Результатов: {len(data.get('results', []))}")
|
||||||
|
if data.get('results'):
|
||||||
|
for item in data['results'][:3]:
|
||||||
|
print(f" - {item.get('text', 'No text')}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Готово!")
|
||||||
|
print("=" * 60)
|
||||||
26
test_customer_api.sh
Normal file
26
test_customer_api.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Тест API эндпоинтов для поиска и создания клиентов
|
||||||
|
|
||||||
|
API_HOST="http://grach.localhost:8000"
|
||||||
|
|
||||||
|
echo "=== Тест API поиска клиентов ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Тест 1: Поиск по имени
|
||||||
|
echo "1. Поиск по имени 'Иван':"
|
||||||
|
curl -s -X GET "${API_HOST}/customers/api/search/?q=Иван" \
|
||||||
|
-H "Accept: application/json" | python -m json.tool || echo "Ошибка в запросе"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. Поиск по частичному номеру телефона '2912':"
|
||||||
|
curl -s -X GET "${API_HOST}/customers/api/search/?q=2912" \
|
||||||
|
-H "Accept: application/json" | python -m json.tool || echo "Ошибка в запросе"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. Поиск по email 'ivan':"
|
||||||
|
curl -s -X GET "${API_HOST}/customers/api/search/?q=ivan" \
|
||||||
|
-H "Accept: application/json" | python -m json.tool || echo "Ошибка в запросе"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Тесты завершены ==="
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
docker run -d \
|
docker run -d `
|
||||||
--name postgres17 \
|
--name postgres17 `
|
||||||
-e POSTGRES_PASSWORD=postgres \
|
-e POSTGRES_PASSWORD=postgres `
|
||||||
-e POSTGRES_USER=postgres \
|
-e POSTGRES_USER=postgres `
|
||||||
-e POSTGRES_DB=inventory_db \
|
-e POSTGRES_DB=inventory_db `
|
||||||
-p 5432:5432 \
|
-p 5432:5432 `
|
||||||
-v postgres17-data:/var/lib/postgresql/data \
|
-v postgres17-data:/var/lib/postgresql/data `
|
||||||
postgres:17
|
postgres:17
|
||||||
|
|
||||||
# 2. Создаем миграции с нуля
|
# 2. Создаем миграции с нуля
|
||||||
python manage.py makemigrations
|
python manage.py makemigrations
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user