Добавлена поддержка черновиков заказов (Этап 1/3)
Реализован базовый функционал для работы с черновиками заказов.
Это первый этап из трех для полной реализации автосохранения.
Изменения:
1. Модель Order (orders/models.py):
- Добавлен статус 'draft' в STATUS_CHOICES
- Добавлено поле last_autosave_at для отслеживания автосохранения
- Добавлен метод is_draft() для проверки статуса черновика
2. Миграция (0005_add_draft_support.py):
- Добавление поля last_autosave_at в Order
- Добавление статуса 'draft' в choices
3. Service Layer (orders/services/draft_service.py):
- Класс DraftOrderService с методами:
* create_draft() - создание нового черновика
* update_draft() - обновление существующего черновика
* add_item_to_draft() - добавление товара в черновик
* remove_item_from_draft() - удаление товара из черновика
* finalize_draft() - завершение черновика (draft → new)
* get_user_drafts() - получение черновиков пользователя
* delete_old_drafts() - очистка старых черновиков
- Вся бизнес-логика инкапсулирована в сервисный слой
Архитектурные решения:
- Использование service layer pattern для изоляции бизнес-логики
- Транзакционность всех операций с черновиками
- Автоматический пересчет итоговой суммы при изменении товаров
- Связь временных комплектов с черновиками через FK
Следующие этапы (в следующих коммитах):
- Этап 2: AJAX endpoints и модификация views
- Этап 3: JavaScript автосохранение и UI
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
33
myproject/orders/migrations/0005_add_draft_support.py
Normal file
33
myproject/orders/migrations/0005_add_draft_support.py
Normal file
@@ -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='Статус заказа'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -89,6 +89,7 @@ class Order(models.Model):
|
|||||||
|
|
||||||
# Статус заказа
|
# Статус заказа
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
|
('draft', 'Черновик'),
|
||||||
('new', 'Новый'),
|
('new', 'Новый'),
|
||||||
('confirmed', 'Подтвержден'),
|
('confirmed', 'Подтвержден'),
|
||||||
('in_assembly', 'В сборке'),
|
('in_assembly', 'В сборке'),
|
||||||
@@ -104,6 +105,14 @@ class Order(models.Model):
|
|||||||
verbose_name="Статус заказа"
|
verbose_name="Статус заказа"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Автосохранение (для черновиков)
|
||||||
|
last_autosave_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Последнее автосохранение",
|
||||||
|
help_text="Время последнего автоматического сохранения черновика"
|
||||||
|
)
|
||||||
|
|
||||||
# Оплата
|
# Оплата
|
||||||
PAYMENT_METHOD_CHOICES = [
|
PAYMENT_METHOD_CHOICES = [
|
||||||
('cash_to_courier', 'Наличные курьеру'),
|
('cash_to_courier', 'Наличные курьеру'),
|
||||||
@@ -299,6 +308,10 @@ class Order(models.Model):
|
|||||||
self.is_paid = False
|
self.is_paid = False
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def is_draft(self):
|
||||||
|
"""Проверяет, является ли заказ черновиком"""
|
||||||
|
return self.status == 'draft'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def amount_due(self):
|
def amount_due(self):
|
||||||
"""Остаток к оплате"""
|
"""Остаток к оплате"""
|
||||||
|
|||||||
7
myproject/orders/services/__init__.py
Normal file
7
myproject/orders/services/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
!5@28A=K9 A;>9 4;O ?@8;>65=8O orders.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .draft_service import DraftOrderService
|
||||||
|
|
||||||
|
__all__ = ['DraftOrderService']
|
||||||
284
myproject/orders/services/draft_service.py
Normal file
284
myproject/orders/services/draft_service.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user