Files
octopus/myproject/products/services/kit_pricing.py
Andrey Smakotin e54d7d04d7 feat: Добавлены команды управления данными тенантов и исправлены фильтры по статусу товаров
Добавлено:
- Команда clear_tenant_data для полной очистки данных тенанта без удаления схемы
  * Очищает все таблицы через TRUNCATE CASCADE
  * Сбрасывает ID-последовательности
  * Сохраняет схему БД и запись Client
  * Поддержка флага --noinput для автоматизации

- Команда init_tenant_data для инициализации системных данных тенанта
  * Создаёт системного клиента (АНОНИМНЫЙ ПОКУПАТЕЛЬ для POS)
  * Создаёт 8 системных статусов заказов
  * Создаёт 5 системных способов оплаты
  * Поддержка флага --reset для пересоздания данных

Исправлено:
- Заменены устаревшие фильтры is_active на status='active' для Product и ProductKit
  * products/views/category_views.py: исправлены фильтры в build_category_tree и get_context_data
  * products/services/kit_pricing.py: исправлены фильтры при получении товаров из variant_group
  * products/models/kits.py: исправлен фильтр в get_available_products
  * Устранена ошибка FieldError при работе со списком категорий

Улучшено:
- Команда clear_tenant_data теперь предлагает пользователю инициализировать системные данные после очистки
- Добавлена детальная информация о процессе очистки и инициализации данных
2025-12-12 04:58:26 +03:00

232 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервисы для расчета цен комплектов (ProductKit).
Извлекает сложную бизнес-логику из модели.
"""
from decimal import Decimal, InvalidOperation
import logging
logger = logging.getLogger(__name__)
class KitPriceCalculator:
"""
Калькулятор цен для ProductKit.
Реализует различные методы ценообразования комплектов.
"""
@staticmethod
def calculate_price_with_substitutions(kit, stock_manager=None):
"""
Расчёт цены комплекта с учётом доступных замен компонентов.
Метод определяет цену комплекта, учитывая доступные товары-заменители
и применяет выбранный метод ценообразования.
Args:
kit (ProductKit): Комплект для расчета
stock_manager: Объект управления складом (если не указан, используется стандартный)
Returns:
Decimal: Расчетная цена комплекта, или 0 в случае ошибки
"""
from ..utils.stock_manager import StockManager
if stock_manager is None:
stock_manager = StockManager()
# Если указана ручная цена, используем её
if kit.pricing_method == 'manual' and kit.price:
return kit.price
total_cost = Decimal('0.00')
total_sale = Decimal('0.00')
for kit_item in kit.kit_items.select_related('product', 'variant_group'):
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 best_product:
item_cost = best_product.cost_price
item_price = best_product.price
item_quantity = kit_item.quantity or Decimal('1.00')
# Проверяем корректность значений перед умножением
if item_cost and item_quantity:
total_cost += item_cost * item_quantity
if item_price and item_quantity:
total_sale += item_price * item_quantity
except (AttributeError, TypeError, InvalidOperation) as e:
# Логируем ошибку, но продолжаем вычисления
logger.warning(
f"Ошибка при расчёте цены для комплекта {kit.name} (item: {kit_item}): {e}"
)
continue # Пропускаем ошибочный элемент и продолжаем с остальными
# Применяем метод ценообразования
try:
if kit.pricing_method == 'from_sale_prices':
return total_sale
elif kit.pricing_method == 'from_cost_plus_percent' and kit.markup_percent is not None:
return total_cost * (Decimal('1') + kit.markup_percent / Decimal('100'))
elif kit.pricing_method == 'from_cost_plus_amount' and kit.markup_amount is not None:
return total_cost + kit.markup_amount
elif kit.pricing_method == 'manual' and kit.price:
return kit.price
return total_sale
except (TypeError, InvalidOperation) as e:
logger.error(
f"Ошибка при применении метода ценообразования для комплекта {kit.name}: {e}"
)
# Возвращаем ручную цену если есть, иначе 0
if kit.pricing_method == 'manual' and kit.price:
return kit.price
return Decimal('0.00')
class KitCostCalculator:
"""
Калькулятор себестоимости для ProductKit.
Включает расчет и валидацию себестоимости комплекта.
"""
@staticmethod
def calculate_cost(kit):
"""
Расчёт себестоимости комплекта на основе себестоимости компонентов.
Args:
kit (ProductKit): Комплект для расчета
Returns:
Decimal: Себестоимость комплекта (может быть 0 если есть проблемы)
"""
total_cost = Decimal('0.00')
for kit_item in kit.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(status='active').first()
if product and product.cost_price:
item_cost = product.cost_price
item_quantity = kit_item.quantity or Decimal('1.00')
total_cost += item_cost * item_quantity
return total_cost
@staticmethod
def validate_and_calculate_cost(kit):
"""
Расчёт себестоимости с полной валидацией.
Проверяет, что все компоненты имеют себестоимость > 0.
Args:
kit (ProductKit): Комплект для валидации и расчета
Returns:
dict: {
'total_cost': Decimal or None,
'is_valid': bool,
'problems': list of dicts {
'component_name': str,
'reason': str,
'kit_item_id': int
}
}
"""
total_cost = Decimal('0.00')
problems = []
if not kit.kit_items.exists():
# Комплект без компонентов не может иметь корректную себестоимость
return {
'total_cost': None,
'is_valid': False,
'problems': [{
'component_name': 'Комплект',
'reason': 'Комплект не содержит компонентов'
}]
}
for kit_item in kit.kit_items.select_related('product', 'variant_group'):
# Получаем продукт
product = kit_item.product
product_name = ''
if not product and kit_item.variant_group:
# Берем первый активный продукт из группы вариантов
product = kit_item.variant_group.products.filter(status='active').first()
if kit_item.variant_group:
product_name = f"[Варианты] {kit_item.variant_group.name}"
if not product:
# Товар не найден или группа вариантов пуста
if kit_item.variant_group:
problems.append({
'component_name': f"[Варианты] {kit_item.variant_group.name}",
'reason': 'Группа не содержит активных товаров',
'kit_item_id': kit_item.id
})
else:
problems.append({
'component_name': 'Неизвестный компонент',
'reason': 'Товар не выбран и нет группы вариантов',
'kit_item_id': kit_item.id
})
continue
# Используем имя товара, если не установили выше
if not product_name:
product_name = product.name
# Проверяем наличие себестоимости
if product.cost_price is None:
problems.append({
'component_name': product_name,
'reason': 'Себестоимость не определена',
'kit_item_id': kit_item.id
})
continue
# Проверяем, что себестоимость > 0
if product.cost_price == Decimal('0.00') or product.cost_price <= 0:
problems.append({
'component_name': product_name,
'reason': 'Себестоимость равна 0',
'kit_item_id': kit_item.id
})
continue
# Если всё OK - добавляем в сумму
try:
item_quantity = kit_item.quantity or Decimal('1.00')
if item_quantity > 0:
total_cost += product.cost_price * item_quantity
except (TypeError, InvalidOperation) as e:
logger.warning(
f"Ошибка при расчете себестоимости компонента {product_name} "
f"комплекта {kit.name}: {e}"
)
problems.append({
'component_name': product_name,
'reason': 'Ошибка при расчете',
'kit_item_id': kit_item.id
})
# Если есть проблемы, себестоимость не валидна
is_valid = len(problems) == 0
return {
'total_cost': total_cost if is_valid else None,
'is_valid': is_valid,
'problems': problems
}