Обновили шапку и вывод всехтоваров. Добавили фильтры

This commit is contained in:
2025-10-24 23:11:29 +03:00
parent 2fb6253d06
commit 9ad9f604e9
35 changed files with 2498 additions and 1270 deletions

View File

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