Добавлено: - Команда 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 теперь предлагает пользователю инициализировать системные данные после очистки - Добавлена детальная информация о процессе очистки и инициализации данных
232 lines
10 KiB
Python
232 lines
10 KiB
Python
"""
|
||
Сервисы для расчета цен комплектов (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
|
||
}
|