Обновили шапку и вывод всехтоваров. Добавили фильтры
This commit is contained in:
@@ -523,6 +523,43 @@ class ProductKit(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
"""Валидация комплекта перед сохранением"""
|
||||
# Проверка соответствия метода ценообразования полям
|
||||
if self.pricing_method == 'fixed' and not self.fixed_price:
|
||||
raise ValidationError({
|
||||
'fixed_price': 'Для метода ценообразования "fixed" необходимо указать фиксированную цену.'
|
||||
})
|
||||
|
||||
if self.pricing_method == 'from_cost_plus_percent' and (
|
||||
self.markup_percent is None or self.markup_percent < 0
|
||||
):
|
||||
raise ValidationError({
|
||||
'markup_percent': 'Для метода ценообразования "from_cost_plus_percent" необходимо указать процент наценки >= 0.'
|
||||
})
|
||||
|
||||
if self.pricing_method == 'from_cost_plus_amount' and (
|
||||
self.markup_amount is None or self.markup_amount < 0
|
||||
):
|
||||
raise ValidationError({
|
||||
'markup_amount': 'Для метода ценообразования "from_cost_plus_amount" необходимо указать сумму наценки >= 0.'
|
||||
})
|
||||
|
||||
# Проверка уникальности SKU (если задан)
|
||||
if self.sku:
|
||||
# Проверяем, что SKU не используется другим комплектом (если объект уже существует)
|
||||
if self.pk:
|
||||
if ProductKit.objects.filter(sku=self.sku).exclude(pk=self.pk).exists():
|
||||
raise ValidationError({
|
||||
'sku': f'Артикул "{self.sku}" уже используется другим комплектом.'
|
||||
})
|
||||
else:
|
||||
# Для новых объектов просто проверяем, что SKU не используется
|
||||
if ProductKit.objects.filter(sku=self.sku).exists():
|
||||
raise ValidationError({
|
||||
'sku': f'Артикул "{self.sku}" уже используется другим комплектом.'
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
from unidecode import unidecode
|
||||
@@ -541,15 +578,30 @@ class ProductKit(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_total_components_count(self):
|
||||
"""Возвращает количество позиций в букете"""
|
||||
"""
|
||||
Возвращает количество компонентов (строк) в комплекте.
|
||||
|
||||
Returns:
|
||||
int: Количество компонентов в комплекте
|
||||
"""
|
||||
return self.kit_items.count()
|
||||
|
||||
def get_components_with_variants_count(self):
|
||||
"""Возвращает количество позиций с группами вариантов"""
|
||||
"""
|
||||
Возвращает количество компонентов, которые используют группы вариантов.
|
||||
|
||||
Returns:
|
||||
int: Количество компонентов с группами вариантов
|
||||
"""
|
||||
return self.kit_items.filter(variant_group__isnull=False).count()
|
||||
|
||||
def get_sale_price(self):
|
||||
"""Возвращает рассчитанную цену продажи комплекта"""
|
||||
"""
|
||||
Возвращает рассчитанную цену продажи комплекта в соответствии с выбранным методом ценообразования.
|
||||
|
||||
Returns:
|
||||
Decimal: Цена продажи комплекта
|
||||
"""
|
||||
try:
|
||||
return self.calculate_price_with_substitutions()
|
||||
except Exception:
|
||||
@@ -560,8 +612,16 @@ class ProductKit(models.Model):
|
||||
|
||||
def check_availability(self, stock_manager=None):
|
||||
"""
|
||||
Проверяет доступность всего букета.
|
||||
Букет доступен, если для каждой позиции есть хотя бы один доступный вариант.
|
||||
Проверяет доступность всего комплекта.
|
||||
|
||||
Комплект доступен, если для каждой позиции в комплекте
|
||||
есть хотя бы один доступный вариант товара.
|
||||
|
||||
Args:
|
||||
stock_manager: Объект управления складом (если не указан, используется стандартный)
|
||||
|
||||
Returns:
|
||||
bool: True, если комплект полностью доступен, иначе False
|
||||
"""
|
||||
from .utils.stock_manager import StockManager
|
||||
|
||||
@@ -577,10 +637,18 @@ class ProductKit(models.Model):
|
||||
|
||||
def calculate_price_with_substitutions(self, stock_manager=None):
|
||||
"""
|
||||
Расчёт цены букета с учётом доступных замен.
|
||||
Использует цены фактически доступных товаров.
|
||||
Расчёт цены комплекта с учётом доступных замен компонентов.
|
||||
|
||||
Метод определяет цену комплекта, учитывая доступные товары-заменители
|
||||
и применяет выбранный метод ценообразования.
|
||||
|
||||
Args:
|
||||
stock_manager: Объект управления складом (если не указан, используется стандартный)
|
||||
|
||||
Returns:
|
||||
Decimal: Расчетная цена комплекта, или 0 в случае ошибки
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from .utils.stock_manager import StockManager
|
||||
|
||||
if stock_manager is None:
|
||||
@@ -594,28 +662,75 @@ class ProductKit(models.Model):
|
||||
total_sale = Decimal('0.00')
|
||||
|
||||
for kit_item in self.kit_items.select_related('product', 'variant_group'):
|
||||
best_product = kit_item.get_best_available_product(stock_manager)
|
||||
try:
|
||||
best_product = kit_item.get_best_available_product(stock_manager)
|
||||
|
||||
if not best_product:
|
||||
# Если товар недоступен, используем цену первого в списке
|
||||
available_products = kit_item.get_available_products()
|
||||
best_product = available_products[0] if available_products else None
|
||||
if not best_product:
|
||||
# Если товар недоступен, используем цену первого в списке
|
||||
available_products = kit_item.get_available_products()
|
||||
best_product = available_products[0] if available_products else None
|
||||
|
||||
if best_product:
|
||||
total_cost += best_product.cost_price * kit_item.quantity
|
||||
total_sale += best_product.sale_price * kit_item.quantity
|
||||
if best_product:
|
||||
item_cost = best_product.cost_price
|
||||
item_sale = best_product.sale_price
|
||||
item_quantity = kit_item.quantity or Decimal('1.00') # По умолчанию 1, если количество не указано
|
||||
|
||||
# Проверяем корректность значений перед умножением
|
||||
if item_cost and item_quantity:
|
||||
total_cost += item_cost * item_quantity
|
||||
if item_sale and item_quantity:
|
||||
total_sale += item_sale * item_quantity
|
||||
except (AttributeError, TypeError, InvalidOperation) as e:
|
||||
# Логируем ошибку, но продолжаем вычисления
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Ошибка при расчёте цены для комплекта {self.name} (item: {kit_item}): {e}")
|
||||
continue # Пропускаем ошибочный элемент и продолжаем с остальными
|
||||
|
||||
# Применяем метод ценообразования
|
||||
if self.pricing_method == 'from_sale_prices':
|
||||
return total_sale
|
||||
elif self.pricing_method == 'from_cost_plus_percent' and self.markup_percent:
|
||||
return total_cost * (Decimal('1') + self.markup_percent / Decimal('100'))
|
||||
elif self.pricing_method == 'from_cost_plus_amount' and self.markup_amount:
|
||||
return total_cost + self.markup_amount
|
||||
elif self.pricing_method == 'fixed' and self.fixed_price:
|
||||
return self.fixed_price
|
||||
try:
|
||||
if self.pricing_method == 'from_sale_prices':
|
||||
return total_sale
|
||||
elif self.pricing_method == 'from_cost_plus_percent' and self.markup_percent is not None:
|
||||
return total_cost * (Decimal('1') + self.markup_percent / Decimal('100'))
|
||||
elif self.pricing_method == 'from_cost_plus_amount' and self.markup_amount is not None:
|
||||
return total_cost + self.markup_amount
|
||||
elif self.pricing_method == 'fixed' and self.fixed_price:
|
||||
return self.fixed_price
|
||||
|
||||
return total_sale
|
||||
return total_sale
|
||||
except (TypeError, InvalidOperation) as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Ошибка при применении метода ценообразования для комплекта {self.name}: {e}")
|
||||
# Возвращаем фиксированную цену если есть, иначе 0
|
||||
if self.pricing_method == 'fixed' and self.fixed_price:
|
||||
return self.fixed_price
|
||||
return Decimal('0.00')
|
||||
|
||||
def calculate_cost(self):
|
||||
"""
|
||||
Расчёт себестоимости комплекта на основе себестоимости компонентов.
|
||||
|
||||
Returns:
|
||||
Decimal: Себестоимость комплекта
|
||||
"""
|
||||
from decimal import Decimal
|
||||
total_cost = Decimal('0.00')
|
||||
|
||||
for kit_item in self.kit_items.select_related('product', 'variant_group'):
|
||||
# Получаем продукт - либо конкретный, либо первый из группы вариантов
|
||||
product = kit_item.product
|
||||
if not product and kit_item.variant_group:
|
||||
# Берем первый продукт из группы вариантов
|
||||
product = kit_item.variant_group.products.filter(is_active=True).first()
|
||||
|
||||
if product:
|
||||
item_cost = product.cost_price
|
||||
item_quantity = kit_item.quantity or Decimal('1.00') # По умолчанию 1, если количество не указано
|
||||
total_cost += item_cost * item_quantity
|
||||
|
||||
return total_cost
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Soft delete вместо hard delete - марк как удаленный"""
|
||||
@@ -663,6 +778,13 @@ class KitItem(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Компонент комплекта"
|
||||
verbose_name_plural = "Компоненты комплектов"
|
||||
indexes = [
|
||||
models.Index(fields=['kit']),
|
||||
models.Index(fields=['product']),
|
||||
models.Index(fields=['variant_group']),
|
||||
models.Index(fields=['kit', 'product']),
|
||||
models.Index(fields=['kit', 'variant_group']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.kit.name} - {self.get_display_name()}"
|
||||
@@ -679,17 +801,36 @@ class KitItem(models.Model):
|
||||
)
|
||||
|
||||
def get_display_name(self):
|
||||
"""Возвращает название для отображения (товар или группа)"""
|
||||
"""
|
||||
Возвращает строку для отображения названия компонента.
|
||||
|
||||
Returns:
|
||||
str: Название компонента (либо группа вариантов, либо конкретный товар)
|
||||
"""
|
||||
if self.variant_group:
|
||||
return f"[Варианты] {self.variant_group.name}"
|
||||
return self.product.name if self.product else "Не указан"
|
||||
|
||||
def has_priorities_set(self):
|
||||
"""Проверяет, настроены ли приоритеты"""
|
||||
"""
|
||||
Проверяет, настроены ли приоритеты замены для данного компонента.
|
||||
|
||||
Returns:
|
||||
bool: True, если приоритеты установлены, иначе False
|
||||
"""
|
||||
return self.priorities.exists()
|
||||
|
||||
def get_available_products(self):
|
||||
"""Возвращает список доступных товаров с учётом приоритетов"""
|
||||
"""
|
||||
Возвращает список доступных товаров для этого компонента.
|
||||
|
||||
Если указан конкретный товар - возвращает его.
|
||||
Если указаны приоритеты - возвращает товары в порядке приоритета.
|
||||
Если не указаны приоритеты - возвращает все активные товары из группы вариантов.
|
||||
|
||||
Returns:
|
||||
list: Список доступных товаров
|
||||
"""
|
||||
if self.product:
|
||||
# Если указан конкретный товар, возвращаем только его
|
||||
return [self.product]
|
||||
@@ -707,7 +848,15 @@ class KitItem(models.Model):
|
||||
return []
|
||||
|
||||
def get_best_available_product(self, stock_manager=None):
|
||||
"""Возвращает первый доступный товар по приоритету"""
|
||||
"""
|
||||
Возвращает первый доступный товар по приоритету, соответствующий требованиям по количеству.
|
||||
|
||||
Args:
|
||||
stock_manager: Объект управления складом (если не указан, используется стандартный)
|
||||
|
||||
Returns:
|
||||
Product or None: Первый доступный товар или None, если ничего не доступно
|
||||
"""
|
||||
from .utils.stock_manager import StockManager
|
||||
|
||||
if stock_manager is None:
|
||||
|
||||
Reference in New Issue
Block a user