Files
octopus/myproject/orders/services/draft_service.py
Andrey Smakotin d3ac875a0e fix: Исправлена работа черновиков заказов и добавлено автосохранение статуса
Проблема 1: Ошибка 500 при создании черновика заказа
- Поле status в модели Order является ForeignKey на OrderStatus
- В коде использовались строковые значения 'draft' и 'new' вместо объектов
- Это приводило к TypeError при создании/обновлении заказов

Решение:
- В DraftOrderService.create_draft: добавлен get_or_create для статуса 'draft'
- В DraftOrderService.finalize_draft: добавлен get_or_create для статуса 'new'
- В DraftOrderService.get_user_drafts: заменен фильтр status='draft' на status__code='draft'
- В DraftOrderService.delete_old_drafts: заменен фильтр status='draft' на status__code='draft'
- В cleanup_draft_orders.py: исправлен фильтр в режиме dry-run

Проблема 2: Отсутствие автосохранения при изменении статуса
- Поле status не отслеживалось в autosave.js
- При смене статуса заказ не сохранялся автоматически

Решение:
- Добавлено поле 'select[name="status"]' в список отслеживаемых полей
- Добавлен сбор значения статуса в функции collectFormData
- Добавлено 'status': 'orders.OrderStatus' в fk_fields для обработки на сервере

Дополнительно:
- Добавлено автосохранение полей адреса доставки (улица, дом, квартира и т.д.)
- Добавлено автосохранение полей получателя (имя, телефон)
- Добавлена автоматическая установка address_mode='new' при наличии адреса

Файлы:
- orders/services/draft_service.py
- orders/management/commands/cleanup_draft_orders.py
- orders/static/orders/js/autosave.js

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 23:00:24 +03:00

405 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервис для работы с черновиками заказов.
Содержит бизнес-логику создания, обновления и завершения черновиков.
"""
from django.db import transaction
from django.utils import timezone
from django.core.exceptions import ValidationError
from decimal import Decimal
import decimal
from datetime import datetime, date, time
from ..models import Order, OrderItem, Address
from products.models import Product, ProductKit
from .address_service import AddressService
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():
# Получаем или создаем статус 'draft'
from ..models import OrderStatus
draft_status, _ = OrderStatus.objects.get_or_create(
code='draft',
defaults={
'name': 'Черновик',
'label': 'Черновик',
'is_system': True,
'color': '#808080',
}
)
order = Order.objects.create(
customer=customer,
status=draft_status,
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)
# Обновляем только переданные поля
# ForeignKey поля требуют специальной обработки
fk_fields = {
'customer': 'customers.Customer',
'pickup_shop': 'shops.Shop',
'status': 'orders.OrderStatus',
}
simple_fields = [
'is_delivery', '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'
]
# Обрабатываем ForeignKey поля
for field_name, model_path in fk_fields.items():
if field_name in data and data[field_name]:
# Получаем модель
app_label, model_name = model_path.split('.')
from django.apps import apps
Model = apps.get_model(app_label, model_name)
# Получаем объект по ID
try:
instance = Model.objects.get(pk=data[field_name])
setattr(order, field_name, instance)
except Model.DoesNotExist:
pass # Игнорируем несуществующие объекты
# === Обработка адреса доставки ===
# Новая логика с выбором режима адреса
if 'address_mode' in data:
address = AddressService.process_address_from_form(order, data)
if address:
# Если адрес не существует в БД, сохраняем его
if not address.pk:
address.save()
order.delivery_address = address
else:
# Если режим "без адреса", удаляем существующий адрес
if order.delivery_address:
old_address = order.delivery_address
order.delivery_address = None
# Удаляем старый адрес если он больше не используется
if old_address and not old_address.order:
old_address.delete()
elif 'delivery_address' in data and data['delivery_address']:
# Старая логика для совместимости (если передается delivery_address напрямую)
try:
address = Address.objects.get(pk=data['delivery_address'])
order.delivery_address = address
except Address.DoesNotExist:
pass
# Обрабатываем простые поля
for field in simple_fields:
if field in data:
value = data[field]
# Конвертируем boolean поля
if field in ['is_delivery', 'customer_is_recipient', 'is_anonymous']:
# Явно конвертируем в bool, обрабатывая различные типы данных
original_value = value
if isinstance(value, bool):
value = value
elif isinstance(value, str):
value = value.lower() in ('true', '1', 'yes', 'on')
elif value is None:
value = False
else:
value = bool(value)
# Логируем для отладки
if field == 'is_delivery':
import logging
logger = logging.getLogger(__name__)
logger.info(f"[AUTOSAVE] is_delivery: original={original_value} (type={type(original_value)}), converted={value}")
# Конвертируем числовые поля в Decimal
elif field in ['delivery_cost', 'discount_amount']:
if value == '' or value is None:
value = None
else:
try:
value = Decimal(str(value))
except (ValueError, TypeError, decimal.InvalidOperation):
value = Decimal('0')
# Конвертируем дату
elif field == 'delivery_date':
if value == '' or value is None:
value = None
elif isinstance(value, str):
try:
value = datetime.strptime(value, '%Y-%m-%d').date()
except ValueError:
value = None
# Конвертируем время
elif field in ['delivery_time_start', 'delivery_time_end']:
if value == '' or value is None:
value = None
elif isinstance(value, str):
try:
# Формат времени может быть HH:MM или HH:MM:SS
if len(value.split(':')) == 2:
value = datetime.strptime(value, '%H:%M').time()
else:
value = datetime.strptime(value, '%H:%M:%S').time()
except ValueError:
value = None
setattr(order, field, value)
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)
# Определяем товар или комплект
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)
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'
from ..models import OrderStatus
new_status, _ = OrderStatus.objects.get_or_create(
code='new',
defaults={
'name': 'Новый',
'label': 'Новый',
'is_system': True,
'color': '#0d6efd',
}
)
# Изменяем статус на 'new'
order.status = new_status
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__code='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__code='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