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

@@ -0,0 +1,3 @@
"""
Валидаторы для products приложения.
"""

View File

@@ -0,0 +1,147 @@
"""
Валидаторы для ProductKit модели.
Извлекает логику валидации из метода clean().
"""
from decimal import Decimal
from django.core.exceptions import ValidationError
import logging
logger = logging.getLogger(__name__)
class KitValidator:
"""
Валидатор для проверки корректности данных ProductKit.
"""
@staticmethod
def validate_pricing_method(kit):
"""
Проверяет соответствие метода ценообразования заданным полям.
Args:
kit (ProductKit): Комплект для валидации
Raises:
ValidationError: Если данные не соответствуют выбранному методу ценообразования
"""
# Проверка соответствия метода ценообразования полям
if kit.pricing_method == 'manual' and not kit.price:
raise ValidationError({
'price': 'Для метода ценообразования "Ручная цена" необходимо указать цену.'
})
if kit.pricing_method == 'from_cost_plus_percent' and (
kit.markup_percent is None or kit.markup_percent < 0
):
raise ValidationError({
'markup_percent': 'Для метода ценообразования "from_cost_plus_percent" необходимо указать процент наценки >= 0.'
})
if kit.pricing_method == 'from_cost_plus_amount' and (
kit.markup_amount is None or kit.markup_amount < 0
):
raise ValidationError({
'markup_amount': 'Для метода ценообразования "from_cost_plus_amount" необходимо указать сумму наценки >= 0.'
})
@staticmethod
def validate_sku_uniqueness(kit):
"""
Проверяет уникальность SKU комплекта.
Args:
kit (ProductKit): Комплект для валидации
Raises:
ValidationError: Если SKU уже используется другим комплектом
"""
if not kit.sku:
return
# Импортируем здесь, чтобы избежать циклических зависимостей
from ..models.kits import ProductKit
# Проверяем, что SKU не используется другим комплектом (если объект уже существует)
if kit.pk:
if ProductKit.objects.filter(sku=kit.sku).exclude(pk=kit.pk).exists():
raise ValidationError({
'sku': f'Артикул "{kit.sku}" уже используется другим комплектом.'
})
else:
# Для новых объектов просто проверяем, что SKU не используется
if ProductKit.objects.filter(sku=kit.sku).exists():
raise ValidationError({
'sku': f'Артикул "{kit.sku}" уже используется другим комплектом.'
})
@staticmethod
def validate_pricing_method_availability(kit):
"""
Проверяет, доступны ли методы ценообразования на основе данных себестоимости.
Если себестоимость компонентов неполная, блокирует методы:
- 'from_cost_plus_percent'
- 'from_cost_plus_amount'
И переключает на 'from_sale_prices' с предупреждением.
Args:
kit (ProductKit): Комплект для валидации
Returns:
tuple: (is_valid: bool, message: str or None)
- is_valid: True если метод ценообразования доступен, False если был переключен
- message: Сообщение об изменении метода ценообразования
"""
# Методы, требующие валидной себестоимости
restricted_methods = ['from_cost_plus_percent', 'from_cost_plus_amount']
# Проверяем валидность себестоимости
cost_info = kit.cost_calculation_info
# Если себестоимость не валидна и выбран ограниченный метод
if not cost_info['is_valid'] and kit.pricing_method in restricted_methods:
# Переключаемся на 'from_sale_prices'
old_method = kit.pricing_method
kit.pricing_method = 'from_sale_prices'
# Формируем сообщение об ошибке
problems_text = ', '.join([
f"{p['component_name']}{p['reason']}"
for p in cost_info['problems']
])
message = (
f"⚠️ Метод ценообразования был переключен с '{KitValidator._get_method_label(old_method)}' "
f"на 'По ценам компонентов', так как не все компоненты имеют полную информацию о себестоимости. "
f"Проблемы: {problems_text}"
)
logger.info(
f"Kit {kit.name} (id={kit.pk}): pricing_method переключен с {old_method} "
f"на from_sale_prices из-за неполной себестоимости"
)
return False, message
return True, None
@staticmethod
def _get_method_label(method_code):
"""
Получить человеческое описание метода ценообразования.
Args:
method_code (str): Код метода ценообразования
Returns:
str: Описание метода
"""
method_labels = {
'manual': 'Ручная цена',
'from_sale_prices': 'По ценам компонентов',
'from_cost_plus_percent': 'Себестоимость + процент',
'from_cost_plus_amount': 'Себестоимость + фиксированная наценка'
}
return method_labels.get(method_code, method_code)