Добавлен статус 'reserved' для витринных букетов ShowcaseItem

- inventory/models.py: добавлен статус 'reserved' в STATUS_CHOICES
- Миграция: 0004_add_reserved_status_to_showcaseitem.py
- Статус reserved используется для витринных букетов в отложенных заказах
- Жизненный цикл: available → in_cart → reserved → sold
This commit is contained in:
2026-01-05 01:37:59 +03:00
parent b1e728f91b
commit 24a64edc82
2 changed files with 106 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.10 on 2026-01-04 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_sale_pending_fields'),
]
operations = [
migrations.AlterField(
model_name='showcaseitem',
name='status',
field=models.CharField(choices=[('available', 'Доступен'), ('in_cart', 'В корзине'), ('reserved', 'Зарезервирован'), ('sold', 'Продан'), ('dismantled', 'Разобран')], db_index=True, default='available', max_length=20, verbose_name='Статус'),
),
]

View File

@@ -584,6 +584,7 @@ class ShowcaseItem(models.Model):
STATUS_CHOICES = [
('available', 'Доступен'),
('in_cart', 'В корзине'),
('reserved', 'Зарезервирован'),
('sold', 'Продан'),
('dismantled', 'Разобран'),
]
@@ -668,10 +669,43 @@ class ShowcaseItem(models.Model):
self.cart_session_id = None
self.save(update_fields=['status', 'locked_by_user', 'cart_lock_expires_at', 'cart_session_id', 'updated_at'])
def reserve_for_order(self, order_item):
"""
Зарезервировать экземпляр под конкретный заказ (жёсткая блокировка).
Используется для отложенных заказов, когда букет привязан к заказу,
но заказ ещё не в финальном статусе.
Args:
order_item: OrderItem - позиция заказа, к которой привязывается экземпляр
Raises:
ValidationError: если экземпляр уже продан или разобран
"""
if self.status == 'sold':
raise ValidationError(f'Экземпляр {self} уже продан')
if self.status == 'dismantled':
raise ValidationError(f'Экземпляр {self} разобран')
self.status = 'reserved'
self.sold_order_item = order_item
# sold_at оставляем пустым - это не финальная продажа
# Очищаем soft-lock поля корзины
self.locked_by_user = None
self.cart_lock_expires_at = None
self.cart_session_id = None
self.save(update_fields=['status', 'sold_order_item', 'locked_by_user',
'cart_lock_expires_at', 'cart_session_id', 'updated_at'])
def mark_sold(self, order_item):
"""
Пометить как проданный.
Проверяет статус перед продажей чтобы избежать дублей.
Для прямой продажи (POS "Продать сейчас"):
available/in_cart -> sold
Для финализации отложенного заказа:
reserved -> sold (через mark_sold_from_reserved)
"""
if self.status == 'sold':
raise ValidationError(f'Экземпляр {self} уже продан')
@@ -684,6 +718,60 @@ class ShowcaseItem(models.Model):
self.cart_session_id = None
self.save()
def mark_sold_from_reserved(self):
"""
Финализировать продажу из зарезервированного состояния.
Используется при переходе заказа в положительный конечный статус (completed).
Raises:
ValidationError: если экземпляр не в статусе 'reserved'
"""
if self.status != 'reserved':
raise ValidationError(
f'Экземпляр {self} не в статусе "reserved" (текущий: {self.get_status_display()})'
)
self.status = 'sold'
self.sold_at = timezone.now()
# sold_order_item уже установлен при резервировании
self.save(update_fields=['status', 'sold_at', 'updated_at'])
def return_to_available(self):
"""
Вернуть экземпляр на витрину (освободить).
Используется при отмене заказа.
Raises:
ValidationError: если экземпляр уже разобран
"""
if self.status == 'dismantled':
raise ValidationError(f'Экземпляр {self} разобран и не может быть возвращён на витрину')
self.status = 'available'
self.sold_order_item = None
self.sold_at = None
self.locked_by_user = None
self.cart_lock_expires_at = None
self.cart_session_id = None
self.save(update_fields=['status', 'sold_order_item', 'sold_at', 'locked_by_user',
'cart_lock_expires_at', 'cart_session_id', 'updated_at'])
def return_to_reserved(self, order_item):
"""
Вернуть экземпляр в зарезервированное состояние.
Используется при откате заказа из completed в нейтральный статус.
Args:
order_item: OrderItem - позиция заказа, за которой остаётся экземпляр
"""
if self.status == 'dismantled':
raise ValidationError(f'Экземпляр {self} разобран')
self.status = 'reserved'
self.sold_order_item = order_item
self.sold_at = None # Сбрасываем дату продажи
self.save(update_fields=['status', 'sold_order_item', 'sold_at', 'updated_at'])
def is_lock_expired(self):
"""Проверить истекла ли блокировка"""
if self.cart_lock_expires_at is None: