diff --git a/myproject/inventory/migrations/0004_add_reserved_status_to_showcaseitem.py b/myproject/inventory/migrations/0004_add_reserved_status_to_showcaseitem.py new file mode 100644 index 0000000..a716469 --- /dev/null +++ b/myproject/inventory/migrations/0004_add_reserved_status_to_showcaseitem.py @@ -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='Статус'), + ), + ] diff --git a/myproject/inventory/models.py b/myproject/inventory/models.py index 7190cbb..0cfcc51 100644 --- a/myproject/inventory/models.py +++ b/myproject/inventory/models.py @@ -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: