From 24a64edc8202ad5904d0749f7526f7aa435543b0 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Mon, 5 Jan 2026 01:37:59 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20'reserved'=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=B1=D1=83=D0=BA=D0=B5=D1=82=D0=BE=D0=B2=20Sho?= =?UTF-8?q?wcaseItem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - inventory/models.py: добавлен статус 'reserved' в STATUS_CHOICES - Миграция: 0004_add_reserved_status_to_showcaseitem.py - Статус reserved используется для витринных букетов в отложенных заказах - Жизненный цикл: available → in_cart → reserved → sold --- ...004_add_reserved_status_to_showcaseitem.py | 18 ++++ myproject/inventory/models.py | 88 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 myproject/inventory/migrations/0004_add_reserved_status_to_showcaseitem.py 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: