Рефакторинг: перенос логики создания временных комплектов в сервис

Изменения:
- Удалена функция 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:
2025-11-10 23:44:05 +03:00
parent 3c0ba70bc8
commit 5d5de1fe31
15 changed files with 471 additions and 150 deletions

View File

@@ -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": []

View File

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

View File

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

View File

@@ -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 компонентов

View 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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -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
View 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
View 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
View 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 "=== Тесты завершены ==="

View File

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