diff --git a/myproject/orders/migrations/0005_add_draft_support.py b/myproject/orders/migrations/0005_add_draft_support.py new file mode 100644 index 0000000..c805412 --- /dev/null +++ b/myproject/orders/migrations/0005_add_draft_support.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.10 on 2025-11-08 15:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0004_orderitem_is_custom_price'), + ] + + operations = [ + migrations.AddField( + model_name='historicalorder', + name='last_autosave_at', + field=models.DateTimeField(blank=True, help_text='Время последнего автоматического сохранения черновика', null=True, verbose_name='Последнее автосохранение'), + ), + migrations.AddField( + model_name='order', + name='last_autosave_at', + field=models.DateTimeField(blank=True, help_text='Время последнего автоматического сохранения черновика', null=True, verbose_name='Последнее автосохранение'), + ), + migrations.AlterField( + model_name='historicalorder', + name='status', + field=models.CharField(choices=[('draft', 'Черновик'), ('new', 'Новый'), ('confirmed', 'Подтвержден'), ('in_assembly', 'В сборке'), ('in_delivery', 'В доставке'), ('delivered', 'Доставлен'), ('cancelled', 'Отменен')], default='new', max_length=20, verbose_name='Статус заказа'), + ), + migrations.AlterField( + model_name='order', + name='status', + field=models.CharField(choices=[('draft', 'Черновик'), ('new', 'Новый'), ('confirmed', 'Подтвержден'), ('in_assembly', 'В сборке'), ('in_delivery', 'В доставке'), ('delivered', 'Доставлен'), ('cancelled', 'Отменен')], default='new', max_length=20, verbose_name='Статус заказа'), + ), + ] diff --git a/myproject/orders/models.py b/myproject/orders/models.py index 6a8a106..d8fa64b 100644 --- a/myproject/orders/models.py +++ b/myproject/orders/models.py @@ -89,6 +89,7 @@ class Order(models.Model): # Статус заказа STATUS_CHOICES = [ + ('draft', 'Черновик'), ('new', 'Новый'), ('confirmed', 'Подтвержден'), ('in_assembly', 'В сборке'), @@ -104,6 +105,14 @@ class Order(models.Model): verbose_name="Статус заказа" ) + # Автосохранение (для черновиков) + last_autosave_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Последнее автосохранение", + help_text="Время последнего автоматического сохранения черновика" + ) + # Оплата PAYMENT_METHOD_CHOICES = [ ('cash_to_courier', 'Наличные курьеру'), @@ -299,6 +308,10 @@ class Order(models.Model): self.is_paid = False self.save() + def is_draft(self): + """Проверяет, является ли заказ черновиком""" + return self.status == 'draft' + @property def amount_due(self): """Остаток к оплате""" diff --git a/myproject/orders/services/__init__.py b/myproject/orders/services/__init__.py new file mode 100644 index 0000000..9454d77 --- /dev/null +++ b/myproject/orders/services/__init__.py @@ -0,0 +1,7 @@ +""" +!5@28A=K9 A;>9 4;O ?@8;>65=8O orders. +""" + +from .draft_service import DraftOrderService + +__all__ = ['DraftOrderService'] diff --git a/myproject/orders/services/draft_service.py b/myproject/orders/services/draft_service.py new file mode 100644 index 0000000..c3b989f --- /dev/null +++ b/myproject/orders/services/draft_service.py @@ -0,0 +1,284 @@ +""" +Сервис для работы с черновиками заказов. +Содержит бизнес-логику создания, обновления и завершения черновиков. +""" +from django.db import transaction +from django.utils import timezone +from django.core.exceptions import ValidationError +from decimal import Decimal + +from ..models import Order, OrderItem +from products.models import Product, ProductKit + + +class DraftOrderService: + """ + Сервис для управления черновиками заказов. + Обеспечивает создание, обновление и финализацию черновиков. + """ + + @staticmethod + def create_draft(user, customer, data=None): + """ + Создает новый черновик заказа. + + Args: + user: Пользователь, создающий заказ + customer: Клиент, для которого создается заказ + data (dict, optional): Дополнительные данные для заказа + + Returns: + Order: Созданный черновик заказа + + Raises: + ValidationError: Если данные невалидны + """ + data = data or {} + + with transaction.atomic(): + order = Order.objects.create( + customer=customer, + status='draft', + modified_by=user, + is_delivery=data.get('is_delivery', True), + delivery_address=data.get('delivery_address'), + pickup_shop=data.get('pickup_shop'), + delivery_date=data.get('delivery_date'), + delivery_time_start=data.get('delivery_time_start'), + delivery_time_end=data.get('delivery_time_end'), + delivery_cost=data.get('delivery_cost', Decimal('0')), + payment_method=data.get('payment_method', 'cash_to_courier'), + customer_is_recipient=data.get('customer_is_recipient', True), + recipient_name=data.get('recipient_name'), + recipient_phone=data.get('recipient_phone'), + is_anonymous=data.get('is_anonymous', False), + special_instructions=data.get('special_instructions'), + last_autosave_at=timezone.now(), + ) + + return order + + @staticmethod + def update_draft(order_id, user, data): + """ + Обновляет существующий черновик заказа. + + Args: + order_id (int): ID заказа + user: Пользователь, изменяющий заказ + data (dict): Данные для обновления + + Returns: + Order: Обновленный заказ + + Raises: + Order.DoesNotExist: Если заказ не найден + ValidationError: Если заказ не является черновиком или данные невалидны + """ + with transaction.atomic(): + order = Order.objects.select_for_update().get(pk=order_id) + + if not order.is_draft(): + raise ValidationError("Можно обновлять только черновики заказов") + + # Обновляем только переданные поля + updatable_fields = [ + 'customer', 'is_delivery', 'delivery_address', 'pickup_shop', + 'delivery_date', 'delivery_time_start', 'delivery_time_end', + 'delivery_cost', 'payment_method', 'customer_is_recipient', + 'recipient_name', 'recipient_phone', 'is_anonymous', + 'special_instructions', 'discount_amount' + ] + + for field in updatable_fields: + if field in data: + setattr(order, field, data[field]) + + order.modified_by = user + order.last_autosave_at = timezone.now() + order.save() + + # Пересчитываем итоговую сумму если изменились товары + if 'recalculate' in data and data['recalculate']: + order.calculate_total() + order.save() + + return order + + @staticmethod + def add_item_to_draft(order_id, product_id=None, product_kit_id=None, quantity=1, price=None): + """ + Добавляет товар или комплект в черновик заказа. + + Args: + order_id (int): ID заказа + product_id (int, optional): ID товара + product_kit_id (int, optional): ID комплекта + quantity (Decimal): Количество + price (Decimal, optional): Цена (если None, берется из товара/комплекта) + + Returns: + OrderItem: Созданная позиция заказа + + Raises: + ValidationError: Если заказ не является черновиком или данные невалидны + """ + with transaction.atomic(): + order = Order.objects.get(pk=order_id) + + if not order.is_draft(): + raise ValidationError("Можно добавлять товары только в черновики заказов") + + # Определяем товар или комплект + product = None + product_kit = None + + if product_id: + product = Product.objects.get(pk=product_id) + if price is None: + price = product.actual_price + elif product_kit_id: + product_kit = ProductKit.objects.get(pk=product_kit_id) + if price is None: + price = product_kit.actual_price + else: + raise ValidationError("Необходимо указать product_id или product_kit_id") + + order_item = OrderItem.objects.create( + order=order, + product=product, + product_kit=product_kit, + quantity=quantity, + price=price + ) + + # Обновляем итоговую сумму заказа + order.calculate_total() + order.last_autosave_at = timezone.now() + order.save() + + return order_item + + @staticmethod + def remove_item_from_draft(order_id, order_item_id): + """ + Удаляет позицию из черновика заказа. + + Args: + order_id (int): ID заказа + order_item_id (int): ID позиции заказа + + Raises: + ValidationError: Если заказ не является черновиком + """ + with transaction.atomic(): + order = Order.objects.get(pk=order_id) + + if not order.is_draft(): + raise ValidationError("Можно удалять товары только из черновиков заказов") + + OrderItem.objects.filter(pk=order_item_id, order=order).delete() + + # Обновляем итоговую сумму заказа + order.calculate_total() + order.last_autosave_at = timezone.now() + order.save() + + @staticmethod + def finalize_draft(order_id, user): + """ + Завершает черновик заказа, переводя его в статус 'new'. + Выполняет финальную валидацию всех данных. + + Args: + order_id (int): ID заказа + user: Пользователь, завершающий заказ + + Returns: + Order: Финализированный заказ + + Raises: + ValidationError: Если данные заказа невалидны или заказ не является черновиком + """ + with transaction.atomic(): + order = Order.objects.select_for_update().get(pk=order_id) + + if not order.is_draft(): + raise ValidationError("Можно финализировать только черновики заказов") + + # Проверяем наличие товаров + if not order.items.exists(): + raise ValidationError("Заказ должен содержать хотя бы один товар") + + # Выполняем полную валидацию модели + order.full_clean() + + # Изменяем статус на 'new' + order.status = 'new' + order.modified_by = user + order.last_autosave_at = None # Очищаем, т.к. заказ больше не черновик + order.save() + + # Привязываем временные комплекты к заказу + ProductKit.objects.filter( + is_temporary=True, + order=order + ).update(order=order) + + return order + + @staticmethod + def get_user_drafts(user, customer=None): + """ + Возвращает черновики заказов пользователя. + + Args: + user: Пользователь + customer (Customer, optional): Фильтр по клиенту + + Returns: + QuerySet: Черновики заказов + """ + drafts = Order.objects.filter( + status='draft', + modified_by=user + ).select_related('customer', 'delivery_address', 'pickup_shop') + + if customer: + drafts = drafts.filter(customer=customer) + + return drafts.order_by('-last_autosave_at') + + @staticmethod + def delete_old_drafts(days=30): + """ + Удаляет старые черновики заказов. + + Args: + days (int): Количество дней, после которых черновик считается старым + + Returns: + int: Количество удаленных черновиков + """ + from datetime import timedelta + + cutoff_date = timezone.now() - timedelta(days=days) + + # Находим старые черновики + old_drafts = Order.objects.filter( + status='draft', + last_autosave_at__lt=cutoff_date + ) + + # Удаляем связанные временные комплекты + for draft in old_drafts: + ProductKit.objects.filter( + is_temporary=True, + order=draft + ).delete() + + # Удаляем черновики + count = old_drafts.count() + old_drafts.delete() + + return count