Files
octopus/myproject/inventory/services/showcase_manager.py
Andrey Smakotin 8f6acfb364 Добавлена функциональность витрин для POS: модели, сервисы, UI
- Создана модель Showcase (витрина) привязанная к складу
- Расширена Reservation для поддержки витринных резервов
- Добавлены поля в OrderItem для маркировки витринных продаж
- Реализован ShowcaseManager с методами резервирования, продажи и разбора
- Обновлён админ-интерфейс для управления витринами
- Добавлена кнопка Витрина в POS (категории) и API для просмотра
- Добавлена кнопка На витрину в панели действий POS
- Миграции готовы к применению
2025-11-16 21:12:22 +03:00

335 lines
14 KiB
Python
Raw 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.
"""
Сервис управления витринами - резервирование, продажа и разбор витринных букетов.
"""
from decimal import Decimal
from django.db import transaction
from django.utils import timezone
from django.core.exceptions import ValidationError
from inventory.models import Showcase, Reservation, Warehouse
from products.models import ProductKit
from orders.models import Order, OrderItem, OrderStatus
from customers.models import Customer
class ShowcaseManager:
"""
Менеджер для работы с витринами и витринными букетами.
"""
@staticmethod
def reserve_kit_to_showcase(product_kit, showcase, quantity=1):
"""
Резервирует комплект на витрину.
Раскладывает комплект на компоненты и создаёт резервы по каждому товару.
Args:
product_kit: ProductKit - комплект для резервирования
showcase: Showcase - витрина
quantity: int - количество комплектов (по умолчанию 1)
Returns:
dict: {
'success': bool,
'reservations': list[Reservation],
'message': str
}
"""
if not showcase.is_active:
return {
'success': False,
'reservations': [],
'message': f'Витрина "{showcase.name}" не активна'
}
warehouse = showcase.warehouse
reservations = []
try:
with transaction.atomic():
# Раскладываем комплект на компоненты
kit_items = product_kit.kit_items.all()
if not kit_items.exists():
return {
'success': False,
'reservations': [],
'message': f'Комплект "{product_kit.name}" не содержит компонентов'
}
# Создаём резервы по каждому компоненту
for kit_item in kit_items:
if kit_item.product:
# Обычный товар
component_quantity = kit_item.quantity * quantity
reservation = Reservation.objects.create(
product=kit_item.product,
warehouse=warehouse,
showcase=showcase,
quantity=component_quantity,
status='reserved'
)
reservations.append(reservation)
elif kit_item.variant_group:
# Группа вариантов - резервируем первый доступный вариант
# В будущем можно добавить выбор конкретного варианта
variant_items = kit_item.variant_group.items.all()
if variant_items.exists():
first_variant = variant_items.first()
component_quantity = kit_item.quantity * quantity
reservation = Reservation.objects.create(
product=first_variant.product,
warehouse=warehouse,
showcase=showcase,
quantity=component_quantity,
status='reserved'
)
reservations.append(reservation)
# Обновляем агрегаты Stock для всех затронутых товаров
from inventory.models import Stock
for reservation in reservations:
stock, _ = Stock.objects.get_or_create(
product=reservation.product,
warehouse=warehouse
)
stock.refresh_from_batches()
return {
'success': True,
'reservations': reservations,
'message': f'Комплект "{product_kit.name}" зарезервирован на витрине "{showcase.name}"'
}
except Exception as e:
return {
'success': False,
'reservations': [],
'message': f'Ошибка резервирования: {str(e)}'
}
@staticmethod
def sell_from_showcase(product_kit, showcase, customer, payment_method='cash_to_courier',
custom_price=None, user=None):
"""
Продаёт комплект с витрины.
Создаёт Order, OrderItem, конвертирует резервы в Sale.
Args:
product_kit: ProductKit - комплект для продажи
showcase: Showcase - витрина
customer: Customer - покупатель
payment_method: str - способ оплаты
custom_price: Decimal - кастомная цена (опционально)
user: CustomUser - пользователь, выполняющий операцию
Returns:
dict: {
'success': bool,
'order': Order or None,
'message': str
}
"""
warehouse = showcase.warehouse
try:
with transaction.atomic():
# Находим резервы для этого комплекта на витрине
# Группируем по product для подсчёта
reservations = Reservation.objects.filter(
showcase=showcase,
status='reserved'
).select_related('product')
if not reservations.exists():
return {
'success': False,
'order': None,
'message': f'На витрине "{showcase.name}" нет зарезервированных товаров'
}
# Получаем статус "Завершён" для POS-продаж
completed_status = OrderStatus.objects.filter(
code='completed',
is_positive_end=True
).first()
if not completed_status:
# Если нет статуса completed, берём любой положительный
completed_status = OrderStatus.objects.filter(
is_positive_end=True
).first()
# Создаём заказ (самовывоз с витринного склада)
order = Order.objects.create(
customer=customer,
is_delivery=False,
pickup_warehouse=warehouse,
status=completed_status,
payment_method=payment_method,
is_paid=True,
modified_by=user
)
# Определяем цену
price = custom_price if custom_price else product_kit.actual_price
is_custom = custom_price is not None
# Создаём позицию заказа
order_item = OrderItem.objects.create(
order=order,
product_kit=product_kit,
quantity=1,
price=price,
is_custom_price=is_custom,
is_from_showcase=True,
showcase=showcase
)
# Привязываем резервы к OrderItem
reservations.update(order_item=order_item)
# Конвертируем резервы в продажи
from inventory.services.sale_processor import SaleProcessor
for reservation in reservations:
# Создаём Sale
sale = SaleProcessor.create_sale_from_reservation(
reservation=reservation,
order=order
)
# Обновляем статус резерва
reservation.status = 'converted_to_sale'
reservation.converted_at = timezone.now()
reservation.save()
# Пересчитываем итоговую сумму заказа
order.calculate_total()
order.amount_paid = order.total_amount
order.update_payment_status()
order.save()
return {
'success': True,
'order': order,
'message': f'Заказ #{order.order_number} создан. Продан комплект с витрины "{showcase.name}"'
}
except Exception as e:
return {
'success': False,
'order': None,
'message': f'Ошибка продажи: {str(e)}'
}
@staticmethod
def dismantle_from_showcase(showcase, product_kit=None):
"""
Разбирает букет на витрине - освобождает резервы.
Args:
showcase: Showcase - витрина
product_kit: ProductKit - конкретный комплект (опционально)
Returns:
dict: {
'success': bool,
'released_count': int,
'message': str
}
"""
try:
with transaction.atomic():
# Находим активные резервы
reservations = Reservation.objects.filter(
showcase=showcase,
status='reserved'
)
if product_kit:
# Если указан конкретный комплект, фильтруем резервы
# TODO: добавить связь резерва с конкретным экземпляром комплекта
# Пока освобождаем все резервы витрины
pass
released_count = reservations.count()
if released_count == 0:
return {
'success': False,
'released_count': 0,
'message': f'На витрине "{showcase.name}" нет активных резервов'
}
# Освобождаем резервы
reservations.update(
status='released',
released_at=timezone.now(),
showcase=None
)
# Обновляем агрегаты Stock
from inventory.models import Stock
affected_products = reservations.values_list('product_id', flat=True).distinct()
for product_id in affected_products:
try:
stock = Stock.objects.get(
product_id=product_id,
warehouse=showcase.warehouse
)
stock.refresh_from_batches()
except Stock.DoesNotExist:
pass
return {
'success': True,
'released_count': released_count,
'message': f'Разобрано {released_count} резервов с витрины "{showcase.name}"'
}
except Exception as e:
return {
'success': False,
'released_count': 0,
'message': f'Ошибка разбора: {str(e)}'
}
@staticmethod
def get_showcase_kits(showcase):
"""
Возвращает список комплектов, зарезервированных на витрине.
Args:
showcase: Showcase
Returns:
list: список словарей с информацией о комплектах
"""
reservations = Reservation.objects.filter(
showcase=showcase,
status='reserved'
).select_related('product').values('product__id', 'product__name', 'quantity')
# Группируем по товарам
products_dict = {}
for res in reservations:
product_id = res['product__id']
if product_id not in products_dict:
products_dict[product_id] = {
'product_name': res['product__name'],
'quantity': Decimal('0')
}
products_dict[product_id]['quantity'] += res['quantity']
return [
{
'product_id': pid,
'product_name': data['product_name'],
'quantity': data['quantity']
}
for pid, data in products_dict.items()
]