fix: Улучшения системы ценообразования комплектов

Исправлены 4 проблемы:
1. Расчёт цены первого товара - улучшена валидация в getProductPrice и calculateFinalPrice
2. Отображение actual_price в Select2 вместо обычной цены
3. Количество по умолчанию = 1 для новых форм компонентов
4. Auto-select текста при клике на поле количества для удобства редактирования

Изменённые файлы:
- products/forms.py: добавлен __init__ в KitItemForm для quantity.initial = 1
- products/templates/includes/select2-product-init.html: обновлена formatSelectResult
- products/templates/productkit_create.html: добавлен focus handler для auto-select
- products/templates/productkit_edit.html: добавлен focus handler для auto-select

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-02 19:04:03 +03:00
parent c84a372f98
commit 6c8af5ab2c
120 changed files with 9035 additions and 3036 deletions

View File

@@ -46,6 +46,7 @@ def search_products_and_variants(request):
'text': f"{product.name} ({product.sku})" if product.sku else product.name,
'sku': product.sku,
'price': str(product.price) if product.price else None,
'actual_price': str(product.actual_price) if product.actual_price else '0',
'in_stock': product.in_stock,
'type': 'product'
}],
@@ -74,19 +75,24 @@ def search_products_and_variants(request):
# Показываем последние добавленные активные товары
products = Product.objects.filter(is_active=True)\
.order_by('-created_at')[:page_size]\
.values('id', 'name', 'sku', 'price', 'in_stock')
.values('id', 'name', 'sku', 'price', 'sale_price', 'in_stock')
for product in products:
text = product['name']
if product['sku']:
text += f" ({product['sku']})"
# Получаем actual_price: приоритет sale_price > price
actual_price = product['sale_price'] if product['sale_price'] else product['price']
results.append({
'id': product['id'],
'text': text,
'sku': product['sku'],
'price': str(product['price']) if product['price'] else None,
'in_stock': product['in_stock']
'actual_price': str(actual_price) if actual_price else '0',
'in_stock': product['in_stock'],
'type': 'product'
})
response_data = {
@@ -147,18 +153,22 @@ def search_products_and_variants(request):
start = (page - 1) * page_size
end = start + page_size
products = products_query[start:end].values('id', 'name', 'sku', 'price', 'in_stock')
products = products_query[start:end].values('id', 'name', 'sku', 'price', 'sale_price', 'in_stock')
for product in products:
text = product['name']
if product['sku']:
text += f" ({product['sku']})"
# Получаем actual_price: приоритет sale_price > price
actual_price = product['sale_price'] if product['sale_price'] else product['price']
results.append({
'id': product['id'],
'text': text,
'sku': product['sku'],
'price': str(product['price']) if product['price'] else None,
'actual_price': str(actual_price) if actual_price else '0',
'in_stock': product['in_stock'],
'type': 'product'
})
@@ -187,3 +197,156 @@ def search_products_and_variants(request):
'results': results,
'pagination': {'more': has_more if search_type == 'product' else False}
})
def validate_kit_cost(request):
"""
AJAX endpoint для валидации себестоимости комплекта в реальном времени.
Принимает список компонентов и возвращает информацию о валидности себестоимости,
доступных методах ценообразования и проблемах.
Request (JSON POST):
{
'components': [
{
'product_id': int or null,
'variant_group_id': int or null,
'quantity': float
},
...
]
}
Response (JSON):
{
'is_valid': bool,
'total_cost': float or null,
'problems': [
{
'component_name': str,
'reason': str
},
...
],
'available_methods': {
'manual': bool,
'from_sale_prices': bool,
'from_cost_plus_percent': bool,
'from_cost_plus_amount': bool
}
}
"""
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
import json
from decimal import Decimal
data = json.loads(request.body)
components = data.get('components', [])
if not components:
return JsonResponse({
'is_valid': False,
'total_cost': None,
'problems': [{
'component_name': 'Комплект',
'reason': 'Комплект не содержит компонентов'
}],
'available_methods': {
'manual': True,
'from_sale_prices': True,
'from_cost_plus_percent': False,
'from_cost_plus_amount': False
}
})
# Валидируем каждый компонент
total_cost = Decimal('0.00')
problems = []
for idx, component in enumerate(components):
product_id = component.get('product_id')
variant_group_id = component.get('variant_group_id')
quantity = Decimal(str(component.get('quantity', 1)))
product = None
product_name = ''
# Получаем товар
if product_id:
try:
product = Product.objects.get(id=product_id)
product_name = product.name
except Product.DoesNotExist:
problems.append({
'component_name': f'Товар #{product_id}',
'reason': 'Товар не найден'
})
continue
elif variant_group_id:
try:
variant_group = ProductVariantGroup.objects.get(id=variant_group_id)
product = variant_group.products.filter(is_active=True).first()
if variant_group:
product_name = f"[Варианты] {variant_group.name}"
except ProductVariantGroup.DoesNotExist:
problems.append({
'component_name': f'Группа вариантов #{variant_group_id}',
'reason': 'Группа не найдена'
})
continue
if not product:
problems.append({
'component_name': product_name or f'Компонент {idx + 1}',
'reason': 'Товар не выбран или группа пуста'
})
continue
# Проверяем себестоимость
if product.cost_price is None:
problems.append({
'component_name': product_name,
'reason': 'Себестоимость не определена'
})
continue
if product.cost_price <= 0:
problems.append({
'component_name': product_name,
'reason': 'Себестоимость равна 0'
})
continue
# Добавляем в сумму
if quantity > 0:
total_cost += product.cost_price * quantity
# Определяем, какие методы доступны
is_cost_valid = len(problems) == 0
available_methods = {
'manual': True,
'from_sale_prices': True,
'from_cost_plus_percent': is_cost_valid,
'from_cost_plus_amount': is_cost_valid
}
return JsonResponse({
'is_valid': is_cost_valid,
'total_cost': float(total_cost) if is_cost_valid else None,
'problems': problems,
'available_methods': available_methods
})
except json.JSONDecodeError:
return JsonResponse({
'error': 'Invalid JSON'
}, status=400)
except Exception as e:
return JsonResponse({
'error': str(e)
}, status=500)